summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/.gitignore2
-rw-r--r--nixos/doc/manual/Makefile30
-rw-r--r--nixos/doc/manual/administration/containers.chapter.md8
-rw-r--r--nixos/doc/manual/administration/running.md14
-rw-r--r--nixos/doc/manual/administration/running.xml21
-rw-r--r--nixos/doc/manual/administration/service-mgmt.chapter.md12
-rw-r--r--nixos/doc/manual/administration/troubleshooting.chapter.md12
-rw-r--r--nixos/doc/manual/configuration/adding-custom-packages.section.md2
-rw-r--r--nixos/doc/manual/configuration/config-file.section.md2
-rw-r--r--nixos/doc/manual/configuration/config-syntax.chapter.md8
-rw-r--r--nixos/doc/manual/configuration/configuration.md27
-rw-r--r--nixos/doc/manual/configuration/configuration.xml31
-rw-r--r--nixos/doc/manual/configuration/declarative-packages.section.md6
-rw-r--r--nixos/doc/manual/configuration/file-systems.chapter.md6
-rw-r--r--nixos/doc/manual/configuration/kubernetes.chapter.md2
-rw-r--r--nixos/doc/manual/configuration/linux-kernel.chapter.md83
-rw-r--r--nixos/doc/manual/configuration/modularity.section.md14
-rw-r--r--nixos/doc/manual/configuration/networking.chapter.md18
-rw-r--r--nixos/doc/manual/configuration/package-mgmt.chapter.md6
-rw-r--r--nixos/doc/manual/configuration/profiles.chapter.md26
-rw-r--r--nixos/doc/manual/configuration/ssh.section.md2
-rw-r--r--nixos/doc/manual/configuration/sshfs-file-systems.section.md2
-rw-r--r--nixos/doc/manual/configuration/user-mgmt.chapter.md2
-rw-r--r--nixos/doc/manual/configuration/wayland.chapter.md2
-rw-r--r--nixos/doc/manual/configuration/x-windows.chapter.md36
-rw-r--r--nixos/doc/manual/configuration/xfce.chapter.md12
-rw-r--r--nixos/doc/manual/contributing-to-this-manual.chapter.md1
-rw-r--r--nixos/doc/manual/default.nix282
-rw-r--r--nixos/doc/manual/development/developing-the-test-driver.chapter.md45
-rw-r--r--nixos/doc/manual/development/development.md15
-rw-r--r--nixos/doc/manual/development/development.xml20
-rw-r--r--nixos/doc/manual/development/freeform-modules.section.md5
-rw-r--r--nixos/doc/manual/development/meta-attributes.section.md6
-rw-r--r--nixos/doc/manual/development/nixos-tests.chapter.md10
-rw-r--r--nixos/doc/manual/development/option-declarations.section.md67
-rw-r--r--nixos/doc/manual/development/option-def.section.md8
-rw-r--r--nixos/doc/manual/development/option-types.section.md51
-rw-r--r--nixos/doc/manual/development/replace-modules.section.md17
-rw-r--r--nixos/doc/manual/development/running-nixos-tests-interactively.section.md33
-rw-r--r--nixos/doc/manual/development/settings-options.section.md18
-rw-r--r--nixos/doc/manual/development/what-happens-during-a-system-switch.chapter.md6
-rw-r--r--nixos/doc/manual/development/writing-documentation.chapter.md4
-rw-r--r--nixos/doc/manual/development/writing-modules.chapter.md34
-rw-r--r--nixos/doc/manual/development/writing-nixos-tests.section.md34
-rw-r--r--nixos/doc/manual/from_md/README.md5
-rw-r--r--nixos/doc/manual/from_md/administration/boot-problems.section.xml144
-rw-r--r--nixos/doc/manual/from_md/administration/cleaning-store.chapter.xml72
-rw-r--r--nixos/doc/manual/from_md/administration/container-networking.section.xml54
-rw-r--r--nixos/doc/manual/from_md/administration/containers.chapter.xml31
-rw-r--r--nixos/doc/manual/from_md/administration/control-groups.chapter.xml67
-rw-r--r--nixos/doc/manual/from_md/administration/declarative-containers.section.xml60
-rw-r--r--nixos/doc/manual/from_md/administration/imperative-containers.section.xml132
-rw-r--r--nixos/doc/manual/from_md/administration/logging.chapter.xml45
-rw-r--r--nixos/doc/manual/from_md/administration/maintenance-mode.section.xml14
-rw-r--r--nixos/doc/manual/from_md/administration/network-problems.section.xml25
-rw-r--r--nixos/doc/manual/from_md/administration/rebooting.chapter.xml38
-rw-r--r--nixos/doc/manual/from_md/administration/rollback.section.xml42
-rw-r--r--nixos/doc/manual/from_md/administration/service-mgmt.chapter.xml141
-rw-r--r--nixos/doc/manual/from_md/administration/store-corruption.section.xml34
-rw-r--r--nixos/doc/manual/from_md/administration/troubleshooting.chapter.xml12
-rw-r--r--nixos/doc/manual/from_md/administration/user-sessions.chapter.xml46
-rw-r--r--nixos/doc/manual/from_md/configuration/abstractions.section.xml101
-rw-r--r--nixos/doc/manual/from_md/configuration/ad-hoc-network-config.section.xml16
-rw-r--r--nixos/doc/manual/from_md/configuration/ad-hoc-packages.section.xml59
-rw-r--r--nixos/doc/manual/from_md/configuration/adding-custom-packages.section.xml118
-rw-r--r--nixos/doc/manual/from_md/configuration/config-file.section.xml231
-rw-r--r--nixos/doc/manual/from_md/configuration/config-syntax.chapter.xml20
-rw-r--r--nixos/doc/manual/from_md/configuration/customizing-packages.section.xml90
-rw-r--r--nixos/doc/manual/from_md/configuration/declarative-packages.section.xml53
-rw-r--r--nixos/doc/manual/from_md/configuration/file-systems.chapter.xml55
-rw-r--r--nixos/doc/manual/from_md/configuration/firewall.section.xml39
-rw-r--r--nixos/doc/manual/from_md/configuration/gpu-accel.chapter.xml281
-rw-r--r--nixos/doc/manual/from_md/configuration/ipv4-config.section.xml43
-rw-r--r--nixos/doc/manual/from_md/configuration/ipv6-config.section.xml47
-rw-r--r--nixos/doc/manual/from_md/configuration/kubernetes.chapter.xml115
-rw-r--r--nixos/doc/manual/from_md/configuration/linux-kernel.chapter.xml204
-rw-r--r--nixos/doc/manual/from_md/configuration/luks-file-systems.section.xml84
-rw-r--r--nixos/doc/manual/from_md/configuration/modularity.section.xml152
-rw-r--r--nixos/doc/manual/from_md/configuration/network-manager.section.xml49
-rw-r--r--nixos/doc/manual/from_md/configuration/networking.chapter.xml15
-rw-r--r--nixos/doc/manual/from_md/configuration/package-mgmt.chapter.xml28
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles.chapter.xml38
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/all-hardware.section.xml15
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/base.section.xml10
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/clone-config.section.xml16
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/demo.section.xml10
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/docker-container.section.xml12
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/graphical.section.xml14
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/hardened.section.xml25
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/headless.section.xml15
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/installation-device.section.xml32
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/minimal.section.xml13
-rw-r--r--nixos/doc/manual/from_md/configuration/profiles/qemu-guest.section.xml11
-rw-r--r--nixos/doc/manual/from_md/configuration/renaming-interfaces.section.xml62
-rw-r--r--nixos/doc/manual/from_md/configuration/ssh.section.xml23
-rw-r--r--nixos/doc/manual/from_md/configuration/sshfs-file-systems.section.xml139
-rw-r--r--nixos/doc/manual/from_md/configuration/subversion.chapter.xml121
-rw-r--r--nixos/doc/manual/from_md/configuration/user-mgmt.chapter.xml105
-rw-r--r--nixos/doc/manual/from_md/configuration/wayland.chapter.xml31
-rw-r--r--nixos/doc/manual/from_md/configuration/wireless.section.xml73
-rw-r--r--nixos/doc/manual/from_md/configuration/x-windows.chapter.xml380
-rw-r--r--nixos/doc/manual/from_md/configuration/xfce.chapter.xml68
-rw-r--r--nixos/doc/manual/from_md/contributing-to-this-manual.chapter.xml52
-rw-r--r--nixos/doc/manual/from_md/development/activation-script.section.xml150
-rw-r--r--nixos/doc/manual/from_md/development/assertions.section.xml58
-rw-r--r--nixos/doc/manual/from_md/development/bootspec.chapter.xml73
-rw-r--r--nixos/doc/manual/from_md/development/building-parts.chapter.xml124
-rw-r--r--nixos/doc/manual/from_md/development/freeform-modules.section.xml87
-rw-r--r--nixos/doc/manual/from_md/development/importing-modules.section.xml47
-rw-r--r--nixos/doc/manual/from_md/development/linking-nixos-tests-to-packages.section.xml10
-rw-r--r--nixos/doc/manual/from_md/development/meta-attributes.section.xml95
-rw-r--r--nixos/doc/manual/from_md/development/nixos-tests.chapter.xml14
-rw-r--r--nixos/doc/manual/from_md/development/option-declarations.section.xml335
-rw-r--r--nixos/doc/manual/from_md/development/option-def.section.xml132
-rw-r--r--nixos/doc/manual/from_md/development/option-types.section.xml1148
-rw-r--r--nixos/doc/manual/from_md/development/replace-modules.section.xml70
-rw-r--r--nixos/doc/manual/from_md/development/running-nixos-tests-interactively.section.xml64
-rw-r--r--nixos/doc/manual/from_md/development/running-nixos-tests.section.xml23
-rw-r--r--nixos/doc/manual/from_md/development/settings-options.section.xml421
-rw-r--r--nixos/doc/manual/from_md/development/sources.chapter.xml90
-rw-r--r--nixos/doc/manual/from_md/development/testing-installer.chapter.xml22
-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-documentation.chapter.xml144
-rw-r--r--nixos/doc/manual/from_md/development/writing-modules.chapter.xml245
-rw-r--r--nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml769
-rw-r--r--nixos/doc/manual/from_md/installation/building-nixos.chapter.xml111
-rw-r--r--nixos/doc/manual/from_md/installation/changing-config.chapter.xml117
-rw-r--r--nixos/doc/manual/from_md/installation/installing-behind-a-proxy.section.xml41
-rw-r--r--nixos/doc/manual/from_md/installation/installing-from-other-distro.section.xml388
-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.xml42
-rw-r--r--nixos/doc/manual/from_md/installation/installing-usb.section.xml135
-rw-r--r--nixos/doc/manual/from_md/installation/installing-virtualbox-guest.section.xml92
-rw-r--r--nixos/doc/manual/from_md/installation/installing.chapter.xml887
-rw-r--r--nixos/doc/manual/from_md/installation/obtaining.chapter.xml47
-rw-r--r--nixos/doc/manual/from_md/installation/upgrading.chapter.xml152
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1310.section.xml6
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1404.section.xml189
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1412.section.xml466
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1509.section.xml776
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1603.section.xml695
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1609.section.xml273
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1703.section.xml818
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1709.section.xml922
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1803.section.xml879
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1809.section.xml941
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1903.section.xml790
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-1909.section.xml1197
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2003.section.xml1497
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2009.section.xml2210
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2105.section.xml1567
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2111.section.xml2122
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2205.section.xml2828
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2211.section.xml1841
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2305.section.xml337
-rw-r--r--nixos/doc/manual/installation/changing-config.chapter.md4
-rw-r--r--nixos/doc/manual/installation/installation.md11
-rw-r--r--nixos/doc/manual/installation/installation.xml18
-rw-r--r--nixos/doc/manual/installation/installing-from-other-distro.section.md42
-rw-r--r--nixos/doc/manual/installation/installing-kexec.section.md2
-rw-r--r--nixos/doc/manual/installation/installing-usb.section.md6
-rw-r--r--nixos/doc/manual/installation/installing-virtualbox-guest.section.md6
-rw-r--r--nixos/doc/manual/installation/installing.chapter.md61
-rw-r--r--nixos/doc/manual/installation/upgrading.chapter.md4
-rw-r--r--nixos/doc/manual/man-configuration.xml31
-rw-r--r--nixos/doc/manual/man-nixos-build-vms.xml138
-rw-r--r--nixos/doc/manual/man-nixos-enter.xml154
-rw-r--r--nixos/doc/manual/man-nixos-generate-config.xml214
-rw-r--r--nixos/doc/manual/man-nixos-install.xml357
-rw-r--r--nixos/doc/manual/man-nixos-option.xml134
-rw-r--r--nixos/doc/manual/man-nixos-rebuild.xml730
-rw-r--r--nixos/doc/manual/man-nixos-version.xml137
-rw-r--r--nixos/doc/manual/man-pages.xml37
-rw-r--r--nixos/doc/manual/manpages/README.md57
-rw-r--r--nixos/doc/manual/manpages/nixos-build-vms.8105
-rw-r--r--nixos/doc/manual/manpages/nixos-enter.872
-rw-r--r--nixos/doc/manual/manpages/nixos-generate-config.8165
-rw-r--r--nixos/doc/manual/manpages/nixos-install.8191
-rw-r--r--nixos/doc/manual/manpages/nixos-option.889
-rw-r--r--nixos/doc/manual/manpages/nixos-rebuild.8452
-rw-r--r--nixos/doc/manual/manpages/nixos-version.886
-rw-r--r--nixos/doc/manual/manual.md56
-rw-r--r--nixos/doc/manual/manual.xml24
-rwxr-xr-xnixos/doc/manual/md-to-db.sh50
-rw-r--r--nixos/doc/manual/nixos-options.md7
-rw-r--r--nixos/doc/manual/preface.md11
-rw-r--r--nixos/doc/manual/preface.xml42
-rw-r--r--nixos/doc/manual/release-notes/release-notes.md25
-rw-r--r--nixos/doc/manual/release-notes/release-notes.xml30
-rw-r--r--nixos/doc/manual/release-notes/rl-1509.section.md12
-rw-r--r--nixos/doc/manual/release-notes/rl-1603.section.md16
-rw-r--r--nixos/doc/manual/release-notes/rl-1609.section.md6
-rw-r--r--nixos/doc/manual/release-notes/rl-1703.section.md10
-rw-r--r--nixos/doc/manual/release-notes/rl-1709.section.md16
-rw-r--r--nixos/doc/manual/release-notes/rl-1803.section.md10
-rw-r--r--nixos/doc/manual/release-notes/rl-1809.section.md10
-rw-r--r--nixos/doc/manual/release-notes/rl-1903.section.md18
-rw-r--r--nixos/doc/manual/release-notes/rl-1909.section.md30
-rw-r--r--nixos/doc/manual/release-notes/rl-2003.section.md58
-rw-r--r--nixos/doc/manual/release-notes/rl-2009.section.md42
-rw-r--r--nixos/doc/manual/release-notes/rl-2105.section.md34
-rw-r--r--nixos/doc/manual/release-notes/rl-2111.section.md2
-rw-r--r--nixos/doc/manual/release-notes/rl-2205.section.md28
-rw-r--r--nixos/doc/manual/release-notes/rl-2305.section.md482
-rw-r--r--nixos/doc/manual/shell.nix8
-rwxr-xr-xnixos/doc/varlistentry-fixer.rb124
-rw-r--r--nixos/doc/xmlformat.conf72
-rw-r--r--nixos/lib/eval-cacheable-options.nix1
-rw-r--r--nixos/lib/eval-config-minimal.nix1
-rw-r--r--nixos/lib/make-channel.nix2
-rw-r--r--nixos/lib/make-disk-image.nix209
-rw-r--r--nixos/lib/make-multi-disk-zfs-image.nix4
-rw-r--r--nixos/lib/make-options-doc/default.nix97
-rw-r--r--nixos/lib/make-options-doc/generateDoc.py108
-rw-r--r--nixos/lib/make-options-doc/mergeJSON.py234
-rw-r--r--nixos/lib/make-options-doc/options-to-docbook.xsl202
-rw-r--r--nixos/lib/make-options-doc/optionsJSONtoXML.nix6
-rw-r--r--nixos/lib/make-options-doc/sortXML.py27
-rw-r--r--nixos/lib/systemd-lib.nix2
-rw-r--r--nixos/lib/systemd-unit-options.nix10
-rw-r--r--nixos/lib/test-driver/default.nix2
-rwxr-xr-xnixos/lib/test-driver/test_driver/__init__.py6
-rw-r--r--nixos/lib/test-driver/test_driver/driver.py30
-rw-r--r--nixos/lib/test-driver/test_driver/logger.py8
-rw-r--r--nixos/lib/test-driver/test_driver/machine.py219
-rw-r--r--nixos/lib/test-driver/test_driver/polling_condition.py29
-rw-r--r--nixos/lib/testing-python.nix10
-rw-r--r--nixos/lib/testing/default.nix5
-rw-r--r--nixos/lib/testing/driver.nix31
-rw-r--r--nixos/lib/testing/legacy.nix3
-rw-r--r--nixos/lib/testing/meta.nix2
-rw-r--r--nixos/lib/testing/nodes.nix10
-rw-r--r--nixos/lib/testing/testScript.nix2
-rw-r--r--nixos/maintainers/scripts/lxd/lxd-image-inner.nix2
-rw-r--r--nixos/modules/config/console.nix25
-rw-r--r--nixos/modules/config/fonts/fontconfig.nix17
-rw-r--r--nixos/modules/config/fonts/fonts.nix38
-rw-r--r--nixos/modules/config/malloc.nix3
-rw-r--r--nixos/modules/config/mysql.nix2
-rw-r--r--nixos/modules/config/no-x-libs.nix26
-rw-r--r--nixos/modules/config/qt.nix (renamed from nixos/modules/config/qt5.nix)40
-rw-r--r--nixos/modules/config/resolvconf.nix4
-rw-r--r--nixos/modules/config/shells-environment.nix4
-rw-r--r--nixos/modules/config/stevenblack.nix34
-rw-r--r--nixos/modules/config/swap.nix36
-rw-r--r--nixos/modules/config/system-environment.nix5
-rw-r--r--nixos/modules/config/update-users-groups.pl10
-rw-r--r--nixos/modules/config/users-groups.nix152
-rw-r--r--nixos/modules/config/zram.nix179
-rw-r--r--nixos/modules/hardware/all-firmware.nix2
-rw-r--r--nixos/modules/hardware/device-tree.nix8
-rw-r--r--nixos/modules/hardware/flipperzero.nix18
-rw-r--r--nixos/modules/hardware/keyboard/qmk.nix16
-rw-r--r--nixos/modules/hardware/keyboard/teck.nix6
-rw-r--r--nixos/modules/hardware/keyboard/uhk.nix7
-rw-r--r--nixos/modules/hardware/keyboard/zsa.nix19
-rw-r--r--nixos/modules/hardware/nitrokey.nix2
-rw-r--r--nixos/modules/hardware/opengl.nix26
-rw-r--r--nixos/modules/hardware/opentabletdriver.nix2
-rw-r--r--nixos/modules/hardware/printers.nix19
-rw-r--r--nixos/modules/hardware/video/hidpi.nix24
-rw-r--r--nixos/modules/hardware/video/nvidia.nix124
-rw-r--r--nixos/modules/hardware/video/webcam/ipu6.nix57
-rw-r--r--nixos/modules/i18n/input-method/default.md160
-rw-r--r--nixos/modules/i18n/input-method/default.nix7
-rw-r--r--nixos/modules/i18n/input-method/default.xml291
-rw-r--r--nixos/modules/i18n/input-method/fcitx.nix46
-rw-r--r--nixos/modules/i18n/input-method/fcitx5.nix38
-rw-r--r--nixos/modules/i18n/input-method/ibus.nix5
-rw-r--r--nixos/modules/i18n/input-method/kime.nix68
-rw-r--r--nixos/modules/installer/cd-dvd/channel.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-base.nix3
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel-no-zfs.nix15
-rw-r--r--nixos/modules/installer/cd-dvd/installation-cd-minimal.nix7
-rw-r--r--nixos/modules/installer/cd-dvd/iso-image.nix61
-rw-r--r--nixos/modules/installer/netboot/netboot-minimal.nix3
-rw-r--r--nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-no-zfs-installer.nix15
-rw-r--r--nixos/modules/installer/sd-card/sd-image-powerpc64le.nix49
-rw-r--r--nixos/modules/installer/tools/nix-fallback-paths.nix10
-rwxr-xr-x[-rw-r--r--]nixos/modules/installer/tools/nixos-enter.sh5
-rw-r--r--nixos/modules/installer/tools/nixos-generate-config.pl41
-rwxr-xr-x[-rw-r--r--]nixos/modules/installer/tools/nixos-install.sh15
-rw-r--r--nixos/modules/installer/tools/nixos-version.sh9
-rw-r--r--nixos/modules/installer/tools/tools.nix19
-rw-r--r--nixos/modules/misc/documentation.nix14
-rw-r--r--nixos/modules/misc/ids.nix6
-rw-r--r--nixos/modules/misc/man-db.nix12
-rw-r--r--nixos/modules/misc/meta.nix2
-rw-r--r--nixos/modules/misc/version.nix57
-rw-r--r--nixos/modules/module-list.nix93
-rw-r--r--nixos/modules/profiles/base.nix17
-rw-r--r--nixos/modules/profiles/installation-device.nix13
-rw-r--r--nixos/modules/profiles/keys/ssh_host_ed25519_key7
-rw-r--r--nixos/modules/profiles/keys/ssh_host_ed25519_key.pub1
-rw-r--r--nixos/modules/profiles/macos-builder.nix239
-rw-r--r--nixos/modules/programs/_1password-gui.nix2
-rw-r--r--nixos/modules/programs/_1password.nix2
-rw-r--r--nixos/modules/programs/atop.nix3
-rw-r--r--nixos/modules/programs/ccache.nix2
-rw-r--r--nixos/modules/programs/clash-verge.nix41
-rw-r--r--nixos/modules/programs/darling.nix21
-rw-r--r--nixos/modules/programs/digitalbitbox/default.md47
-rw-r--r--nixos/modules/programs/digitalbitbox/default.nix2
-rw-r--r--nixos/modules/programs/digitalbitbox/doc.xml74
-rw-r--r--nixos/modules/programs/firefox.nix4
-rw-r--r--nixos/modules/programs/flashrom.nix3
-rw-r--r--nixos/modules/programs/fzf.nix19
-rw-r--r--nixos/modules/programs/gamescope.nix85
-rw-r--r--nixos/modules/programs/git.nix38
-rw-r--r--nixos/modules/programs/gnome-documents.nix54
-rw-r--r--nixos/modules/programs/gnupg.nix2
-rw-r--r--nixos/modules/programs/hyprland.nix81
-rw-r--r--nixos/modules/programs/i3lock.nix58
-rw-r--r--nixos/modules/programs/iay.nix37
-rw-r--r--nixos/modules/programs/java.nix24
-rw-r--r--nixos/modules/programs/k3b.nix4
-rw-r--r--nixos/modules/programs/less.nix2
-rw-r--r--nixos/modules/programs/minipro.nix29
-rw-r--r--nixos/modules/programs/miriway.nix78
-rw-r--r--nixos/modules/programs/neovim.nix47
-rw-r--r--nixos/modules/programs/nexttrace.nix25
-rw-r--r--nixos/modules/programs/nix-index.nix62
-rw-r--r--nixos/modules/programs/nix-ld.nix29
-rw-r--r--nixos/modules/programs/plotinus.md17
-rw-r--r--nixos/modules/programs/plotinus.nix2
-rw-r--r--nixos/modules/programs/plotinus.xml30
-rw-r--r--nixos/modules/programs/proxychains.nix8
-rw-r--r--nixos/modules/programs/qdmr.nix25
-rw-r--r--nixos/modules/programs/regreet.nix75
-rw-r--r--nixos/modules/programs/sharing.nix19
-rw-r--r--nixos/modules/programs/singularity.nix102
-rw-r--r--nixos/modules/programs/skim.nix4
-rw-r--r--nixos/modules/programs/ssh.nix11
-rw-r--r--nixos/modules/programs/starship.nix25
-rw-r--r--nixos/modules/programs/steam.nix105
-rw-r--r--nixos/modules/programs/streamdeck-ui.nix2
-rw-r--r--nixos/modules/programs/sway.nix24
-rw-r--r--nixos/modules/programs/tmux.nix21
-rw-r--r--nixos/modules/programs/tsm-client.nix6
-rw-r--r--nixos/modules/programs/waybar.nix9
-rw-r--r--nixos/modules/programs/wireshark.nix2
-rw-r--r--nixos/modules/programs/xastir.nix23
-rw-r--r--nixos/modules/programs/zsh/oh-my-zsh.md109
-rw-r--r--nixos/modules/programs/zsh/oh-my-zsh.nix2
-rw-r--r--nixos/modules/programs/zsh/oh-my-zsh.xml155
-rw-r--r--nixos/modules/programs/zsh/zsh-syntax-highlighting.nix1
-rw-r--r--nixos/modules/programs/zsh/zsh.nix3
-rw-r--r--nixos/modules/rename.nix9
-rw-r--r--nixos/modules/security/acme/default.md354
-rw-r--r--nixos/modules/security/acme/default.nix12
-rw-r--r--nixos/modules/security/acme/doc.xml414
-rw-r--r--nixos/modules/security/audit.nix2
-rw-r--r--nixos/modules/security/doas.nix20
-rw-r--r--nixos/modules/security/ipa.nix258
-rw-r--r--nixos/modules/security/pam.nix33
-rw-r--r--nixos/modules/security/polkit.nix2
-rw-r--r--nixos/modules/security/systemd-confinement.nix1
-rw-r--r--nixos/modules/services/amqp/activemq/default.nix25
-rw-r--r--nixos/modules/services/audio/gmediarender.nix116
-rw-r--r--nixos/modules/services/audio/gonic.nix89
-rw-r--r--nixos/modules/services/audio/hqplayerd.nix3
-rw-r--r--nixos/modules/services/audio/mpd.nix2
-rw-r--r--nixos/modules/services/audio/navidrome.nix4
-rw-r--r--nixos/modules/services/audio/roon-bridge.nix9
-rw-r--r--nixos/modules/services/audio/roon-server.nix7
-rw-r--r--nixos/modules/services/audio/snapserver.nix4
-rw-r--r--nixos/modules/services/audio/tts.nix151
-rw-r--r--nixos/modules/services/audio/ympd.nix40
-rw-r--r--nixos/modules/services/backup/borgbackup.md163
-rw-r--r--nixos/modules/services/backup/borgbackup.nix17
-rw-r--r--nixos/modules/services/backup/borgbackup.xml209
-rw-r--r--nixos/modules/services/backup/borgmatic.nix87
-rw-r--r--nixos/modules/services/backup/btrbk.nix57
-rw-r--r--nixos/modules/services/backup/mysql-backup.nix2
-rw-r--r--nixos/modules/services/backup/restic.nix33
-rw-r--r--nixos/modules/services/backup/sanoid.nix4
-rw-r--r--nixos/modules/services/backup/syncoid.nix4
-rw-r--r--nixos/modules/services/backup/zfs-replication.nix2
-rw-r--r--nixos/modules/services/blockchain/ethereum/geth.nix4
-rw-r--r--nixos/modules/services/cluster/hadoop/hbase.nix228
-rw-r--r--nixos/modules/services/cluster/k3s/default.nix13
-rw-r--r--nixos/modules/services/cluster/kubernetes/addon-manager.nix2
-rw-r--r--nixos/modules/services/cluster/kubernetes/addons/dns.nix11
-rw-r--r--nixos/modules/services/cluster/kubernetes/kubelet.nix10
-rw-r--r--nixos/modules/services/cluster/kubernetes/pki.nix2
-rw-r--r--nixos/modules/services/computing/boinc/client.nix2
-rw-r--r--nixos/modules/services/computing/slurm/slurm.nix2
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix11
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/worker.nix7
-rw-r--r--nixos/modules/services/continuous-integration/github-runner.nix2
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/options.nix56
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/service.nix398
-rw-r--r--nixos/modules/services/continuous-integration/github-runners.nix2
-rw-r--r--nixos/modules/services/continuous-integration/gitlab-runner.nix102
-rw-r--r--nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix3
-rw-r--r--nixos/modules/services/continuous-integration/hydra/default.nix2
-rw-r--r--nixos/modules/services/continuous-integration/jenkins/job-builder.nix2
-rw-r--r--nixos/modules/services/continuous-integration/woodpecker/agents.nix144
-rw-r--r--nixos/modules/services/continuous-integration/woodpecker/server.nix98
-rw-r--r--nixos/modules/services/databases/clickhouse.nix2
-rw-r--r--nixos/modules/services/databases/dgraph.nix4
-rw-r--r--nixos/modules/services/databases/foundationdb.md309
-rw-r--r--nixos/modules/services/databases/foundationdb.nix2
-rw-r--r--nixos/modules/services/databases/foundationdb.xml443
-rw-r--r--nixos/modules/services/databases/lldap.nix121
-rw-r--r--nixos/modules/services/databases/neo4j.nix2
-rw-r--r--nixos/modules/services/databases/postgresql.md210
-rw-r--r--nixos/modules/services/databases/postgresql.nix29
-rw-r--r--nixos/modules/services/databases/postgresql.xml231
-rw-r--r--nixos/modules/services/desktops/deepin/app-services.nix36
-rw-r--r--nixos/modules/services/desktops/deepin/dde-api.nix50
-rw-r--r--nixos/modules/services/desktops/deepin/dde-daemon.nix40
-rw-r--r--nixos/modules/services/desktops/flatpak.md39
-rw-r--r--nixos/modules/services/desktops/flatpak.nix2
-rw-r--r--nixos/modules/services/desktops/flatpak.xml56
-rw-r--r--nixos/modules/services/desktops/gnome/evolution-data-server.nix4
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json39
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/client.conf.json31
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json28
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/jack.conf.json38
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json120
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json38
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json103
-rw-r--r--nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json97
-rw-r--r--nixos/modules/services/desktops/pipewire/media-session/alsa-monitor.conf.json34
-rw-r--r--nixos/modules/services/desktops/pipewire/media-session/bluez-monitor.conf.json36
-rw-r--r--nixos/modules/services/desktops/pipewire/media-session/media-session.conf.json68
-rw-r--r--nixos/modules/services/desktops/pipewire/media-session/v4l2-monitor.conf.json30
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire-media-session.nix141
-rw-r--r--nixos/modules/services/desktops/pipewire/pipewire.nix111
-rw-r--r--nixos/modules/services/desktops/pipewire/wireplumber.nix4
-rw-r--r--nixos/modules/services/desktops/system76-scheduler.nix296
-rw-r--r--nixos/modules/services/development/blackfire.md39
-rw-r--r--nixos/modules/services/development/blackfire.nix2
-rw-r--r--nixos/modules/services/development/blackfire.xml46
-rw-r--r--nixos/modules/services/development/gemstash.nix103
-rw-r--r--nixos/modules/services/development/lorri.nix2
-rw-r--r--nixos/modules/services/development/zammad.nix2
-rw-r--r--nixos/modules/services/display-managers/greetd.nix2
-rw-r--r--nixos/modules/services/editors/emacs.md420
-rw-r--r--nixos/modules/services/editors/emacs.nix2
-rw-r--r--nixos/modules/services/editors/emacs.xml580
-rw-r--r--nixos/modules/services/games/freeciv.nix2
-rw-r--r--nixos/modules/services/games/minetest-server.nix2
-rw-r--r--nixos/modules/services/hardware/asusd.nix64
-rw-r--r--nixos/modules/services/hardware/auto-cpufreq.nix21
-rw-r--r--nixos/modules/services/hardware/bluetooth.nix27
-rw-r--r--nixos/modules/services/hardware/fancontrol.nix7
-rw-r--r--nixos/modules/services/hardware/fwupd.nix36
-rw-r--r--nixos/modules/services/hardware/kanata.nix90
-rw-r--r--nixos/modules/services/hardware/keyd.nix112
-rw-r--r--nixos/modules/services/hardware/sane.nix4
-rw-r--r--nixos/modules/services/hardware/supergfxd.nix6
-rw-r--r--nixos/modules/services/hardware/throttled.nix8
-rw-r--r--nixos/modules/services/hardware/trezord.md17
-rw-r--r--nixos/modules/services/hardware/trezord.nix2
-rw-r--r--nixos/modules/services/hardware/trezord.xml26
-rw-r--r--nixos/modules/services/hardware/udev.nix13
-rw-r--r--nixos/modules/services/hardware/udisks2.nix24
-rw-r--r--nixos/modules/services/hardware/undervolt.nix4
-rw-r--r--nixos/modules/services/home-automation/esphome.nix136
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix14
-rw-r--r--nixos/modules/services/logging/logrotate.nix7
-rw-r--r--nixos/modules/services/logging/syslogd.nix2
-rw-r--r--nixos/modules/services/logging/ulogd.nix48
-rw-r--r--nixos/modules/services/logging/vector.nix11
-rw-r--r--nixos/modules/services/mail/dovecot.nix14
-rw-r--r--nixos/modules/services/mail/exim.nix5
-rw-r--r--nixos/modules/services/mail/goeland.nix74
-rw-r--r--nixos/modules/services/mail/listmonk.nix2
-rw-r--r--nixos/modules/services/mail/maddy.nix166
-rw-r--r--nixos/modules/services/mail/mailman.md82
-rw-r--r--nixos/modules/services/mail/mailman.nix12
-rw-r--r--nixos/modules/services/mail/mailman.xml94
-rw-r--r--nixos/modules/services/mail/postfix.nix4
-rw-r--r--nixos/modules/services/mail/public-inbox.nix6
-rw-r--r--nixos/modules/services/mail/roundcube.nix26
-rw-r--r--nixos/modules/services/mail/zeyple.nix125
-rw-r--r--nixos/modules/services/matrix/appservice-discord.nix16
-rw-r--r--nixos/modules/services/matrix/dendrite.nix15
-rw-r--r--nixos/modules/services/matrix/mautrix-facebook.nix2
-rw-r--r--nixos/modules/services/matrix/mautrix-telegram.nix2
-rw-r--r--nixos/modules/services/matrix/mjolnir.md110
-rw-r--r--nixos/modules/services/matrix/mjolnir.nix2
-rw-r--r--nixos/modules/services/matrix/mjolnir.xml134
-rw-r--r--nixos/modules/services/matrix/mx-puppet-discord.nix (renamed from nixos/modules/services/misc/mx-puppet-discord.nix)0
-rw-r--r--nixos/modules/services/matrix/synapse.md213
-rw-r--r--nixos/modules/services/matrix/synapse.nix16
-rw-r--r--nixos/modules/services/matrix/synapse.xml276
-rw-r--r--nixos/modules/services/misc/atuin.nix9
-rw-r--r--nixos/modules/services/misc/autorandr.nix14
-rw-r--r--nixos/modules/services/misc/autosuspend.nix230
-rw-r--r--nixos/modules/services/misc/etebase-server.nix8
-rw-r--r--nixos/modules/services/misc/fstrim.nix2
-rw-r--r--nixos/modules/services/misc/gammu-smsd.nix10
-rw-r--r--nixos/modules/services/misc/gitea.nix262
-rw-r--r--nixos/modules/services/misc/gitit.nix725
-rw-r--r--nixos/modules/services/misc/gitlab.md112
-rw-r--r--nixos/modules/services/misc/gitlab.nix224
-rw-r--r--nixos/modules/services/misc/gitlab.xml151
-rw-r--r--nixos/modules/services/misc/gpsd.nix48
-rw-r--r--nixos/modules/services/misc/input-remapper.nix4
-rw-r--r--nixos/modules/services/misc/jellyseerr.nix62
-rw-r--r--nixos/modules/services/misc/klipper.nix2
-rw-r--r--nixos/modules/services/misc/mbpfan.nix41
-rw-r--r--nixos/modules/services/misc/moonraker.nix51
-rw-r--r--nixos/modules/services/misc/n8n.nix7
-rw-r--r--nixos/modules/services/misc/nitter.nix9
-rw-r--r--nixos/modules/services/misc/nix-daemon.nix15
-rw-r--r--nixos/modules/services/misc/ntfy-sh.nix5
-rw-r--r--nixos/modules/services/misc/octoprint.nix3
-rw-r--r--nixos/modules/services/misc/paperless.nix112
-rw-r--r--nixos/modules/services/misc/polaris.nix2
-rw-r--r--nixos/modules/services/misc/portunus.nix2
-rw-r--r--nixos/modules/services/misc/pufferpanel.nix176
-rw-r--r--nixos/modules/services/misc/pykms.nix2
-rw-r--r--nixos/modules/services/misc/readarr.nix88
-rw-r--r--nixos/modules/services/misc/redmine.nix14
-rw-r--r--nixos/modules/services/misc/siproxd.nix2
-rw-r--r--nixos/modules/services/misc/snapper.nix2
-rw-r--r--nixos/modules/services/misc/sourcehut/default.md93
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix4
-rw-r--r--nixos/modules/services/misc/sourcehut/sourcehut.xml119
-rw-r--r--nixos/modules/services/misc/sssd.nix5
-rw-r--r--nixos/modules/services/misc/tandoor-recipes.nix1
-rw-r--r--nixos/modules/services/misc/taskserver/default.md93
-rw-r--r--nixos/modules/services/misc/taskserver/default.nix2
-rw-r--r--nixos/modules/services/misc/taskserver/doc.xml135
-rw-r--r--nixos/modules/services/misc/weechat.md46
-rw-r--r--nixos/modules/services/misc/weechat.nix4
-rw-r--r--nixos/modules/services/misc/weechat.xml66
-rw-r--r--nixos/modules/services/misc/zoneminder.nix12
-rw-r--r--nixos/modules/services/monitoring/apcupsd.nix17
-rw-r--r--nixos/modules/services/monitoring/cadvisor.nix2
-rw-r--r--nixos/modules/services/monitoring/cockpit.nix231
-rw-r--r--nixos/modules/services/monitoring/datadog-agent.nix2
-rw-r--r--nixos/modules/services/monitoring/grafana-agent.nix24
-rw-r--r--nixos/modules/services/monitoring/grafana-image-renderer.nix4
-rw-r--r--nixos/modules/services/monitoring/grafana.nix26
-rw-r--r--nixos/modules/services/monitoring/loki.nix6
-rw-r--r--nixos/modules/services/monitoring/mackerel-agent.nix2
-rw-r--r--nixos/modules/services/monitoring/mimir.nix9
-rw-r--r--nixos/modules/services/monitoring/netdata.nix16
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.md5
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.nix54
-rw-r--r--nixos/modules/services/monitoring/parsedmarc.xml125
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix107
-rw-r--r--nixos/modules/services/monitoring/prometheus/default.nix4
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.md180
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.nix3
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters.xml248
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/collectd.nix4
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/pihole.nix36
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix2
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/shelly.nix27
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix12
-rw-r--r--nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix6
-rw-r--r--nixos/modules/services/monitoring/tuptime.nix6
-rw-r--r--nixos/modules/services/monitoring/uptime-kuma.nix12
-rw-r--r--nixos/modules/services/network-filesystems/kubo.nix134
-rw-r--r--nixos/modules/services/network-filesystems/litestream/default.md (renamed from nixos/modules/services/network-filesystems/litestream/litestream.xml)33
-rw-r--r--nixos/modules/services/network-filesystems/litestream/default.nix3
-rw-r--r--nixos/modules/services/network-filesystems/moosefs.nix6
-rw-r--r--nixos/modules/services/network-filesystems/openafs/lib.nix4
-rw-r--r--nixos/modules/services/network-filesystems/openafs/server.nix86
-rw-r--r--nixos/modules/services/networking/avahi-daemon.nix41
-rw-r--r--nixos/modules/services/networking/bind.nix19
-rw-r--r--nixos/modules/services/networking/bird-lg.nix112
-rw-r--r--nixos/modules/services/networking/blockbook-frontend.nix2
-rw-r--r--nixos/modules/services/networking/blocky.nix1
-rw-r--r--nixos/modules/services/networking/cgit.nix203
-rw-r--r--nixos/modules/services/networking/cloudflared.nix17
-rw-r--r--nixos/modules/services/networking/consul.nix8
-rw-r--r--nixos/modules/services/networking/ddclient.nix5
-rw-r--r--nixos/modules/services/networking/dhcpcd.nix22
-rw-r--r--nixos/modules/services/networking/dhcpd.nix7
-rw-r--r--nixos/modules/services/networking/envoy.nix51
-rw-r--r--nixos/modules/services/networking/firefox-syncserver.nix8
-rw-r--r--nixos/modules/services/networking/firefox-syncserver.xml77
-rw-r--r--nixos/modules/services/networking/firewall-iptables.nix334
-rw-r--r--nixos/modules/services/networking/firewall-nftables.nix179
-rw-r--r--nixos/modules/services/networking/firewall.nix592
-rw-r--r--nixos/modules/services/networking/gnunet.nix4
-rw-r--r--nixos/modules/services/networking/go-neb.nix3
-rw-r--r--nixos/modules/services/networking/headscale.nix797
-rw-r--r--nixos/modules/services/networking/hostapd.nix7
-rw-r--r--nixos/modules/services/networking/imaginary.nix113
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/builder.sh1
-rw-r--r--nixos/modules/services/networking/iscsi/root-initiator.nix4
-rw-r--r--nixos/modules/services/networking/ivpn.nix51
-rw-r--r--nixos/modules/services/networking/jicofo.nix48
-rw-r--r--nixos/modules/services/networking/kresd.nix6
-rw-r--r--nixos/modules/services/networking/minidlna.nix38
-rw-r--r--nixos/modules/services/networking/mosquitto.nix4
-rw-r--r--nixos/modules/services/networking/mosquitto.xml147
-rw-r--r--nixos/modules/services/networking/multipath.nix9
-rw-r--r--nixos/modules/services/networking/murmur.nix30
-rw-r--r--nixos/modules/services/networking/nat-iptables.nix191
-rw-r--r--nixos/modules/services/networking/nat-nftables.nix184
-rw-r--r--nixos/modules/services/networking/nat.nix351
-rw-r--r--nixos/modules/services/networking/ndppd.nix2
-rw-r--r--nixos/modules/services/networking/nebula.nix71
-rw-r--r--nixos/modules/services/networking/netbird.nix5
-rw-r--r--nixos/modules/services/networking/networkd-dispatcher.nix98
-rw-r--r--nixos/modules/services/networking/nftables.nix71
-rw-r--r--nixos/modules/services/networking/nomad.nix26
-rw-r--r--nixos/modules/services/networking/ntopng.nix2
-rw-r--r--nixos/modules/services/networking/ntp/chrony.nix56
-rw-r--r--nixos/modules/services/networking/openconnect.nix6
-rw-r--r--nixos/modules/services/networking/openvpn.nix32
-rw-r--r--nixos/modules/services/networking/peroxide.nix131
-rw-r--r--nixos/modules/services/networking/picosnitch.nix26
-rw-r--r--nixos/modules/services/networking/pixiecore.nix10
-rw-r--r--nixos/modules/services/networking/pleroma.md180
-rw-r--r--nixos/modules/services/networking/pleroma.nix4
-rw-r--r--nixos/modules/services/networking/pleroma.xml188
-rw-r--r--nixos/modules/services/networking/powerdns.nix22
-rw-r--r--nixos/modules/services/networking/prosody.md72
-rw-r--r--nixos/modules/services/networking/prosody.nix3
-rw-r--r--nixos/modules/services/networking/prosody.xml87
-rw-r--r--nixos/modules/services/networking/radicale.nix8
-rw-r--r--nixos/modules/services/networking/redsocks.nix3
-rw-r--r--nixos/modules/services/networking/rpcbind.nix12
-rw-r--r--nixos/modules/services/networking/shellhub-agent.nix2
-rw-r--r--nixos/modules/services/networking/smokeping.nix17
-rw-r--r--nixos/modules/services/networking/soju.nix2
-rw-r--r--nixos/modules/services/networking/ssh/lshd.nix6
-rw-r--r--nixos/modules/services/networking/ssh/sshd.nix304
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix51
-rw-r--r--nixos/modules/services/networking/strongswan.nix6
-rw-r--r--nixos/modules/services/networking/stunnel.nix4
-rw-r--r--nixos/modules/services/networking/syncthing.nix23
-rw-r--r--nixos/modules/services/networking/tailscale.nix4
-rw-r--r--nixos/modules/services/networking/teleport.nix12
-rw-r--r--nixos/modules/services/networking/tinc.nix169
-rw-r--r--nixos/modules/services/networking/tmate-ssh-server.nix2
-rw-r--r--nixos/modules/services/networking/tox-node.nix2
-rw-r--r--nixos/modules/services/networking/unbound.nix2
-rw-r--r--nixos/modules/services/networking/unifi.nix10
-rw-r--r--nixos/modules/services/networking/v2raya.nix49
-rw-r--r--nixos/modules/services/networking/vdirsyncer.nix2
-rw-r--r--nixos/modules/services/networking/webhook.nix214
-rw-r--r--nixos/modules/services/networking/wg-quick.nix8
-rw-r--r--nixos/modules/services/networking/wgautomesh.nix161
-rw-r--r--nixos/modules/services/networking/wireguard.nix4
-rw-r--r--nixos/modules/services/networking/wpa_supplicant.nix14
-rw-r--r--nixos/modules/services/networking/wstunnel.nix429
-rw-r--r--nixos/modules/services/networking/xinetd.nix2
-rw-r--r--nixos/modules/services/networking/yggdrasil.md (renamed from nixos/modules/services/networking/yggdrasil.xml)65
-rw-r--r--nixos/modules/services/networking/yggdrasil.nix202
-rw-r--r--nixos/modules/services/networking/znc/default.nix2
-rw-r--r--nixos/modules/services/printing/cups-pdf.nix185
-rw-r--r--nixos/modules/services/printing/cupsd.nix8
-rw-r--r--nixos/modules/services/search/meilisearch.md12
-rw-r--r--nixos/modules/services/search/meilisearch.nix6
-rw-r--r--nixos/modules/services/search/meilisearch.xml85
-rw-r--r--nixos/modules/services/search/opensearch.nix248
-rw-r--r--nixos/modules/services/search/qdrant.nix129
-rw-r--r--nixos/modules/services/search/solr.nix110
-rw-r--r--nixos/modules/services/security/aesmd.nix21
-rw-r--r--nixos/modules/services/security/authelia.nix401
-rw-r--r--nixos/modules/services/security/fail2ban.nix132
-rw-r--r--nixos/modules/services/security/kanidm.nix104
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix5
-rw-r--r--nixos/modules/services/security/privacyidea.nix4
-rw-r--r--nixos/modules/services/security/vault-agent.nix128
-rw-r--r--nixos/modules/services/security/yubikey-agent.nix3
-rw-r--r--nixos/modules/services/system/cachix-agent/default.nix5
-rw-r--r--nixos/modules/services/system/cachix-watch-store.nix8
-rw-r--r--nixos/modules/services/system/cloud-init.nix286
-rw-r--r--nixos/modules/services/system/dbus.nix21
-rw-r--r--nixos/modules/services/system/nscd.nix10
-rw-r--r--nixos/modules/services/system/self-deploy.nix4
-rw-r--r--nixos/modules/services/torrent/flexget.nix4
-rw-r--r--nixos/modules/services/torrent/magnetico.nix2
-rw-r--r--nixos/modules/services/torrent/rtorrent.nix11
-rw-r--r--nixos/modules/services/torrent/transmission.nix6
-rw-r--r--nixos/modules/services/video/mediamtx.nix (renamed from nixos/modules/services/video/rtsp-simple-server.nix)32
-rw-r--r--nixos/modules/services/video/unifi-video.nix2
-rw-r--r--nixos/modules/services/video/v4l2-relayd.nix199
-rw-r--r--nixos/modules/services/web-apps/akkoma.md332
-rw-r--r--nixos/modules/services/web-apps/akkoma.nix1086
-rw-r--r--nixos/modules/services/web-apps/alps.nix2
-rw-r--r--nixos/modules/services/web-apps/baget.nix170
-rw-r--r--nixos/modules/services/web-apps/changedetection-io.nix2
-rw-r--r--nixos/modules/services/web-apps/chatgpt-retrieval-plugin.nix106
-rw-r--r--nixos/modules/services/web-apps/cloudlog.nix503
-rw-r--r--nixos/modules/services/web-apps/coder.nix217
-rw-r--r--nixos/modules/services/web-apps/dex.nix7
-rw-r--r--nixos/modules/services/web-apps/discourse.md286
-rw-r--r--nixos/modules/services/web-apps/discourse.nix16
-rw-r--r--nixos/modules/services/web-apps/discourse.xml355
-rw-r--r--nixos/modules/services/web-apps/dokuwiki.nix402
-rw-r--r--nixos/modules/services/web-apps/dolibarr.nix21
-rw-r--r--nixos/modules/services/web-apps/freshrss.nix49
-rw-r--r--nixos/modules/services/web-apps/galene.nix2
-rw-r--r--nixos/modules/services/web-apps/grocy.md66
-rw-r--r--nixos/modules/services/web-apps/grocy.nix2
-rw-r--r--nixos/modules/services/web-apps/grocy.xml77
-rw-r--r--nixos/modules/services/web-apps/hedgedoc.nix34
-rw-r--r--nixos/modules/services/web-apps/hledger-web.nix2
-rw-r--r--nixos/modules/services/web-apps/ihatemoney/default.nix153
-rw-r--r--nixos/modules/services/web-apps/jirafeau.nix2
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.md45
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.nix11
-rw-r--r--nixos/modules/services/web-apps/jitsi-meet.xml55
-rw-r--r--nixos/modules/services/web-apps/kasmweb/default.nix275
-rw-r--r--nixos/modules/services/web-apps/kasmweb/initialize_kasmweb.sh114
-rw-r--r--nixos/modules/services/web-apps/kavita.nix83
-rw-r--r--nixos/modules/services/web-apps/keycloak.md141
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix2
-rw-r--r--nixos/modules/services/web-apps/keycloak.xml202
-rw-r--r--nixos/modules/services/web-apps/lemmy.nix12
-rw-r--r--nixos/modules/services/web-apps/lemmy.xml51
-rw-r--r--nixos/modules/services/web-apps/limesurvey.nix35
-rw-r--r--nixos/modules/services/web-apps/mainsail.nix66
-rw-r--r--nixos/modules/services/web-apps/mastodon.nix149
-rw-r--r--nixos/modules/services/web-apps/matomo-doc.xml107
-rw-r--r--nixos/modules/services/web-apps/matomo.md77
-rw-r--r--nixos/modules/services/web-apps/matomo.nix2
-rw-r--r--nixos/modules/services/web-apps/mattermost.nix17
-rw-r--r--nixos/modules/services/web-apps/mediawiki.nix175
-rw-r--r--nixos/modules/services/web-apps/monica.nix468
-rw-r--r--nixos/modules/services/web-apps/moodle.nix2
-rw-r--r--nixos/modules/services/web-apps/netbox.nix164
-rw-r--r--nixos/modules/services/web-apps/nextcloud-notify_push.nix96
-rw-r--r--nixos/modules/services/web-apps/nextcloud.md225
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix143
-rw-r--r--nixos/modules/services/web-apps/nextcloud.xml305
-rw-r--r--nixos/modules/services/web-apps/onlyoffice.nix7
-rw-r--r--nixos/modules/services/web-apps/peertube.nix125
-rw-r--r--nixos/modules/services/web-apps/photoprism.nix155
-rw-r--r--nixos/modules/services/web-apps/pict-rs.md1
-rw-r--r--nixos/modules/services/web-apps/pict-rs.nix4
-rw-r--r--nixos/modules/services/web-apps/pict-rs.xml162
-rw-r--r--nixos/modules/services/web-apps/plausible.md35
-rw-r--r--nixos/modules/services/web-apps/plausible.nix16
-rw-r--r--nixos/modules/services/web-apps/plausible.xml51
-rw-r--r--nixos/modules/services/web-apps/snipe-it.nix5
-rw-r--r--nixos/modules/services/web-apps/wiki-js.nix10
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix176
-rw-r--r--nixos/modules/services/web-apps/writefreely.nix11
-rw-r--r--nixos/modules/services/web-servers/caddy/default.nix3
-rw-r--r--nixos/modules/services/web-servers/fcgiwrap.nix2
-rw-r--r--nixos/modules/services/web-servers/garage.md96
-rw-r--r--nixos/modules/services/web-servers/garage.nix17
-rw-r--r--nixos/modules/services/web-servers/jboss/builder.sh1
-rw-r--r--nixos/modules/services/web-servers/lighttpd/default.nix10
-rw-r--r--nixos/modules/services/web-servers/minio.nix79
-rw-r--r--nixos/modules/services/web-servers/nginx/default.nix298
-rw-r--r--nixos/modules/services/web-servers/nginx/vhost-options.nix40
-rw-r--r--nixos/modules/services/web-servers/stargazer.nix226
-rw-r--r--nixos/modules/services/web-servers/tomcat.nix6
-rw-r--r--nixos/modules/services/x11/desktop-managers/budgie.nix238
-rw-r--r--nixos/modules/services/x11/desktop-managers/cinnamon.nix16
-rw-r--r--nixos/modules/services/x11/desktop-managers/deepin.nix208
-rw-r--r--nixos/modules/services/x11/desktop-managers/default.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.md167
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.nix6
-rw-r--r--nixos/modules/services/x11/desktop-managers/gnome.xml253
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.md74
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.nix15
-rw-r--r--nixos/modules/services/x11/desktop-managers/pantheon.xml120
-rw-r--r--nixos/modules/services/x11/desktop-managers/phosh.nix2
-rw-r--r--nixos/modules/services/x11/desktop-managers/plasma5.nix258
-rw-r--r--nixos/modules/services/x11/display-managers/gdm.nix2
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix4
-rw-r--r--nixos/modules/services/x11/display-managers/sddm.nix10
-rw-r--r--nixos/modules/services/x11/extra-layouts.nix6
-rw-r--r--nixos/modules/services/x11/gdk-pixbuf.nix2
-rw-r--r--nixos/modules/services/x11/picom.nix2
-rw-r--r--nixos/modules/services/x11/window-managers/i3.nix3
-rw-r--r--nixos/modules/services/x11/window-managers/katriawm.nix4
-rw-r--r--nixos/modules/services/x11/window-managers/nimdow.nix23
-rw-r--r--nixos/modules/services/x11/window-managers/qtile.nix45
-rw-r--r--nixos/modules/services/x11/window-managers/stumpwm.nix4
-rw-r--r--nixos/modules/services/x11/xserver.nix44
-rw-r--r--nixos/modules/system/activation/bootspec.cue28
-rw-r--r--nixos/modules/system/activation/bootspec.nix54
-rwxr-xr-xnixos/modules/system/activation/switch-to-configuration.pl2
-rw-r--r--nixos/modules/system/activation/top-level.nix40
-rw-r--r--nixos/modules/system/boot/binfmt.nix38
-rw-r--r--nixos/modules/system/boot/grow-partition.nix5
-rw-r--r--nixos/modules/system/boot/initrd-network.nix6
-rw-r--r--nixos/modules/system/boot/initrd-openvpn.nix26
-rw-r--r--nixos/modules/system/boot/initrd-ssh.nix74
-rw-r--r--nixos/modules/system/boot/kernel.nix44
-rw-r--r--nixos/modules/system/boot/loader/external/external.md2
-rw-r--r--nixos/modules/system/boot/loader/external/external.nix4
-rw-r--r--nixos/modules/system/boot/loader/external/external.xml41
-rw-r--r--nixos/modules/system/boot/loader/grub/grub.nix7
-rw-r--r--nixos/modules/system/boot/loader/grub/install-grub.pl62
-rw-r--r--nixos/modules/system/boot/loader/init-script/init-script-builder.sh6
-rw-r--r--nixos/modules/system/boot/loader/init-script/init-script.nix1
-rwxr-xr-x[-rw-r--r--]nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py92
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix14
-rw-r--r--nixos/modules/system/boot/luksroot.nix78
-rw-r--r--nixos/modules/system/boot/modprobe.nix2
-rw-r--r--nixos/modules/system/boot/networkd.nix1289
-rw-r--r--nixos/modules/system/boot/plymouth.nix3
-rw-r--r--nixos/modules/system/boot/resolved.nix4
-rw-r--r--nixos/modules/system/boot/stage-1-init.sh12
-rw-r--r--nixos/modules/system/boot/stage-1.nix27
-rwxr-xr-xnixos/modules/system/boot/stage-2-init.sh2
-rw-r--r--nixos/modules/system/boot/stage-2.nix1
-rw-r--r--nixos/modules/system/boot/systemd.nix6
-rw-r--r--nixos/modules/system/boot/systemd/coredump.nix16
-rw-r--r--nixos/modules/system/boot/systemd/homed.nix43
-rw-r--r--nixos/modules/system/boot/systemd/initrd-secrets.nix4
-rw-r--r--nixos/modules/system/boot/systemd/initrd.nix118
-rw-r--r--nixos/modules/system/boot/systemd/repart.nix123
-rw-r--r--nixos/modules/system/boot/systemd/user.nix78
-rw-r--r--nixos/modules/system/boot/systemd/userdbd.nix18
-rw-r--r--nixos/modules/system/boot/tmp.nix81
-rw-r--r--nixos/modules/system/etc/setup-etc.pl23
-rw-r--r--nixos/modules/tasks/filesystems.nix52
-rw-r--r--nixos/modules/tasks/filesystems/bcachefs.nix15
-rw-r--r--nixos/modules/tasks/filesystems/envfs.nix60
-rw-r--r--nixos/modules/tasks/filesystems/vfat.nix2
-rw-r--r--nixos/modules/tasks/filesystems/zfs.nix29
-rw-r--r--nixos/modules/tasks/network-interfaces-systemd.nix302
-rw-r--r--nixos/modules/tasks/network-interfaces.nix3
-rw-r--r--nixos/modules/testing/minimal-kernel.nix28
-rw-r--r--nixos/modules/testing/test-instrumentation.nix8
-rw-r--r--nixos/modules/virtualisation/amazon-image.nix8
-rw-r--r--nixos/modules/virtualisation/amazon-options.nix13
-rw-r--r--nixos/modules/virtualisation/azure-agent-entropy.patch17
-rw-r--r--nixos/modules/virtualisation/azure-agent.nix216
-rw-r--r--nixos/modules/virtualisation/azure-common.nix6
-rw-r--r--nixos/modules/virtualisation/brightbox-image.nix2
-rw-r--r--nixos/modules/virtualisation/cloudstack-config.nix2
-rw-r--r--nixos/modules/virtualisation/containers.nix4
-rw-r--r--nixos/modules/virtualisation/cri-o.nix15
-rw-r--r--nixos/modules/virtualisation/digital-ocean-config.nix2
-rw-r--r--nixos/modules/virtualisation/docker.nix4
-rw-r--r--nixos/modules/virtualisation/ec2-metadata-fetcher.sh7
-rw-r--r--nixos/modules/virtualisation/google-compute-config.nix4
-rw-r--r--nixos/modules/virtualisation/libvirtd.nix12
-rw-r--r--nixos/modules/virtualisation/linode-config.nix4
-rw-r--r--nixos/modules/virtualisation/linode-image.nix2
-rw-r--r--nixos/modules/virtualisation/lxc-container.nix10
-rw-r--r--nixos/modules/virtualisation/multipass.nix61
-rw-r--r--nixos/modules/virtualisation/nixos-containers.nix19
-rw-r--r--nixos/modules/virtualisation/oci-containers.nix5
-rw-r--r--nixos/modules/virtualisation/openstack-config.nix4
-rw-r--r--nixos/modules/virtualisation/openstack-options.nix4
-rw-r--r--nixos/modules/virtualisation/parallels-guest.nix1
-rw-r--r--nixos/modules/virtualisation/podman/default.nix107
-rw-r--r--nixos/modules/virtualisation/podman/dnsname.nix36
-rw-r--r--nixos/modules/virtualisation/proxmox-image.nix19
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix441
-rw-r--r--nixos/modules/virtualisation/virtualbox-image.nix63
-rw-r--r--nixos/modules/virtualisation/waydroid.nix6
-rw-r--r--nixos/release-combined.nix7
-rw-r--r--nixos/release-small.nix17
-rw-r--r--nixos/release.nix27
-rw-r--r--nixos/tests/aaaaxy.nix28
-rw-r--r--nixos/tests/acme.nix62
-rw-r--r--nixos/tests/aesmd.nix106
-rw-r--r--nixos/tests/akkoma.nix121
-rw-r--r--nixos/tests/all-tests.nix125
-rw-r--r--nixos/tests/apcupsd.nix41
-rw-r--r--nixos/tests/apfs.nix8
-rw-r--r--nixos/tests/atuin.nix2
-rw-r--r--nixos/tests/authelia.nix169
-rw-r--r--nixos/tests/binary-cache.nix62
-rw-r--r--nixos/tests/bootspec.nix48
-rw-r--r--nixos/tests/borgbackup.nix6
-rw-r--r--nixos/tests/bpf.nix4
-rw-r--r--nixos/tests/btrbk-doas.nix114
-rw-r--r--nixos/tests/btrbk.nix6
-rw-r--r--nixos/tests/budgie.nix56
-rw-r--r--nixos/tests/buildbot.nix4
-rw-r--r--nixos/tests/cage.nix6
-rw-r--r--nixos/tests/ceph-single-node.nix11
-rw-r--r--nixos/tests/cgit.nix73
-rw-r--r--nixos/tests/chrony-ptp.nix28
-rw-r--r--nixos/tests/clickhouse.nix2
-rw-r--r--nixos/tests/cloudlog.nix18
-rw-r--r--nixos/tests/cockpit.nix135
-rw-r--r--nixos/tests/coder.nix24
-rw-r--r--nixos/tests/common/acme/server/generate-certs.nix2
-rw-r--r--nixos/tests/common/ec2.nix9
-rw-r--r--nixos/tests/connman.nix77
-rw-r--r--nixos/tests/consul-template.nix36
-rw-r--r--nixos/tests/consul.nix30
-rw-r--r--nixos/tests/coturn.nix6
-rw-r--r--nixos/tests/cups-pdf.nix40
-rw-r--r--nixos/tests/darling.nix44
-rw-r--r--nixos/tests/deepin.nix57
-rw-r--r--nixos/tests/discourse.nix8
-rw-r--r--nixos/tests/docker-tools.nix54
-rw-r--r--nixos/tests/dokuwiki.nix174
-rw-r--r--nixos/tests/early-mount-options.nix19
-rw-r--r--nixos/tests/elk.nix15
-rw-r--r--nixos/tests/envfs.nix42
-rw-r--r--nixos/tests/envoy.nix35
-rw-r--r--nixos/tests/esphome.nix41
-rw-r--r--nixos/tests/evcc.nix1
-rw-r--r--nixos/tests/fcitx/config12
-rw-r--r--nixos/tests/fcitx/profile4
-rw-r--r--nixos/tests/fcitx5/config11
-rw-r--r--nixos/tests/fcitx5/default.nix (renamed from nixos/tests/fcitx/default.nix)123
-rw-r--r--nixos/tests/fcitx5/profile15
-rw-r--r--nixos/tests/firewall.nix13
-rw-r--r--nixos/tests/freshrss-pgsql.nix48
-rw-r--r--nixos/tests/freshrss-sqlite.nix (renamed from nixos/tests/freshrss.nix)1
-rw-r--r--nixos/tests/fsck.nix12
-rw-r--r--nixos/tests/ft2-clone.nix4
-rw-r--r--nixos/tests/garage/basic.nix98
-rw-r--r--nixos/tests/garage/default.nix53
-rw-r--r--nixos/tests/garage/with-3node-replication.nix (renamed from nixos/tests/garage.nix)58
-rw-r--r--nixos/tests/gemstash.nix51
-rw-r--r--nixos/tests/gitea.nix34
-rw-r--r--nixos/tests/github-runner.nix37
-rw-r--r--nixos/tests/gitlab.nix16
-rw-r--r--nixos/tests/gnome-flashback.nix51
-rw-r--r--nixos/tests/gnome-xorg.nix19
-rw-r--r--nixos/tests/gnome.nix26
-rw-r--r--nixos/tests/gnupg.nix118
-rw-r--r--nixos/tests/gollum.nix2
-rw-r--r--nixos/tests/gonic.nix18
-rw-r--r--nixos/tests/google-oslogin/server.nix4
-rwxr-xr-xnixos/tests/google-oslogin/server.py10
-rw-r--r--nixos/tests/grafana/provision/default.nix12
-rw-r--r--nixos/tests/graylog.nix1
-rw-r--r--nixos/tests/hadoop/hbase.nix25
-rw-r--r--nixos/tests/haproxy.nix1
-rw-r--r--nixos/tests/headscale.nix17
-rw-r--r--nixos/tests/hibernate.nix2
-rw-r--r--nixos/tests/home-assistant.nix116
-rw-r--r--nixos/tests/hostname.nix86
-rw-r--r--nixos/tests/ihatemoney/default.nix71
-rw-r--r--nixos/tests/ihatemoney/rates.json39
-rw-r--r--nixos/tests/ihatemoney/server.crt28
-rw-r--r--nixos/tests/ihatemoney/server.key52
-rw-r--r--nixos/tests/image-contents.nix25
-rw-r--r--nixos/tests/initrd-luks-empty-passphrase.nix97
-rw-r--r--nixos/tests/initrd-network-openvpn/default.nix21
-rw-r--r--nixos/tests/initrd-network-openvpn/initrd.ovpn3
-rw-r--r--nixos/tests/initrd-network-ssh/default.nix4
-rw-r--r--nixos/tests/initrd-secrets-changing.nix57
-rw-r--r--nixos/tests/installed-tests/fwupd.nix2
-rw-r--r--nixos/tests/installed-tests/pipewire.nix12
-rw-r--r--nixos/tests/installer.nix107
-rw-r--r--nixos/tests/isso.nix2
-rw-r--r--nixos/tests/k3s/default.nix8
-rw-r--r--nixos/tests/k3s/multi-node.nix6
-rw-r--r--nixos/tests/k3s/single-node.nix9
-rw-r--r--nixos/tests/kanidm.nix4
-rw-r--r--nixos/tests/kavita.nix36
-rw-r--r--nixos/tests/kea.nix120
-rw-r--r--nixos/tests/keepassxc.nix10
-rw-r--r--nixos/tests/kernel-generic.nix2
-rw-r--r--nixos/tests/keyd.nix82
-rw-r--r--nixos/tests/keymap.nix12
-rw-r--r--nixos/tests/knot.nix48
-rw-r--r--nixos/tests/kubo.nix92
-rw-r--r--nixos/tests/libreswan.nix2
-rw-r--r--nixos/tests/libvirtd.nix2
-rw-r--r--nixos/tests/lldap.nix26
-rw-r--r--nixos/tests/login.nix13
-rw-r--r--nixos/tests/luks.nix69
-rw-r--r--nixos/tests/lvm2/systemd-stage-1.nix8
-rw-r--r--nixos/tests/maddy/default.nix6
-rw-r--r--nixos/tests/maddy/tls.nix94
-rw-r--r--nixos/tests/maddy/unencrypted.nix (renamed from nixos/tests/maddy.nix)14
-rw-r--r--nixos/tests/mate.nix58
-rw-r--r--nixos/tests/matrix/mjolnir.nix5
-rw-r--r--nixos/tests/mattermost.nix16
-rw-r--r--nixos/tests/mediawiki.nix95
-rw-r--r--nixos/tests/mindustry.nix28
-rw-r--r--nixos/tests/minio.nix84
-rw-r--r--nixos/tests/miriway.nix130
-rw-r--r--nixos/tests/mongodb.nix6
-rw-r--r--nixos/tests/multipass.nix37
-rw-r--r--nixos/tests/musescore.nix56
-rw-r--r--nixos/tests/n8n.nix4
-rw-r--r--nixos/tests/nat.nix23
-rw-r--r--nixos/tests/nebula.nix207
-rw-r--r--nixos/tests/netdata.nix2
-rw-r--r--nixos/tests/networking.nix1
-rw-r--r--nixos/tests/nextcloud/default.nix2
-rw-r--r--nixos/tests/nextcloud/openssl-sse.nix5
-rw-r--r--nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix43
-rw-r--r--nixos/tests/nextcloud/with-mysql-and-memcached.nix11
-rw-r--r--nixos/tests/nextcloud/with-postgresql-and-redis.nix33
-rw-r--r--nixos/tests/nginx-http3.nix6
-rw-r--r--nixos/tests/nginx.nix10
-rw-r--r--nixos/tests/nixops/default.nix8
-rw-r--r--nixos/tests/nixos-rebuild-specialisations.nix131
-rw-r--r--nixos/tests/nixos-test-driver/extra-python-packages.nix (renamed from nixos/tests/extra-python-packages.nix)2
-rw-r--r--nixos/tests/nixos-test-driver/node-name.nix33
-rw-r--r--nixos/tests/non-default-filesystems.nix3
-rw-r--r--nixos/tests/noto-fonts-cjk-qt-default-weight.nix30
-rw-r--r--nixos/tests/nscd.nix23
-rw-r--r--nixos/tests/ntfy-sh.nix2
-rw-r--r--nixos/tests/octoprint.nix61
-rw-r--r--nixos/tests/openldap.nix2
-rw-r--r--nixos/tests/opensearch.nix52
-rw-r--r--nixos/tests/pam/test_chfn.py2
-rw-r--r--nixos/tests/pantheon.nix16
-rw-r--r--nixos/tests/paperless.nix4
-rw-r--r--nixos/tests/parsedmarc/default.nix5
-rw-r--r--nixos/tests/pass-secret-service.nix2
-rw-r--r--nixos/tests/peroxide.nix16
-rw-r--r--nixos/tests/pgadmin4-standalone.nix43
-rw-r--r--nixos/tests/pgadmin4.nix180
-rw-r--r--nixos/tests/phosh.nix2
-rw-r--r--nixos/tests/photoprism.nix23
-rw-r--r--nixos/tests/pleroma.nix4
-rw-r--r--nixos/tests/podman/default.nix118
-rw-r--r--nixos/tests/podman/dnsname.nix42
-rw-r--r--nixos/tests/podman/tls-ghostunnel.nix3
-rw-r--r--nixos/tests/pomerium.nix7
-rw-r--r--nixos/tests/postgresql-jit.nix48
-rw-r--r--nixos/tests/postgresql-wal-receiver.nix2
-rw-r--r--nixos/tests/postgresql.nix2
-rw-r--r--nixos/tests/power-profiles-daemon.nix1
-rw-r--r--nixos/tests/pppd.nix4
-rw-r--r--nixos/tests/predictable-interface-names.nix39
-rw-r--r--nixos/tests/printing.nix187
-rw-r--r--nixos/tests/prometheus-exporters.nix14
-rw-r--r--nixos/tests/promscale.nix60
-rw-r--r--nixos/tests/pufferpanel.nix74
-rw-r--r--nixos/tests/qemu-vm-restrictnetwork.nix36
-rw-r--r--nixos/tests/quake3.nix8
-rw-r--r--nixos/tests/readarr.nix18
-rw-r--r--nixos/tests/restic.nix61
-rw-r--r--nixos/tests/sgtpuzzles.nix34
-rw-r--r--nixos/tests/shadow.nix23
-rw-r--r--nixos/tests/soapui.nix2
-rw-r--r--nixos/tests/solr.nix56
-rw-r--r--nixos/tests/sourcehut.nix6
-rw-r--r--nixos/tests/specialisation.nix43
-rw-r--r--nixos/tests/sssd.nix1
-rw-r--r--nixos/tests/stratis/simple.nix2
-rw-r--r--nixos/tests/swap-file-btrfs.nix46
-rw-r--r--nixos/tests/swap-partition.nix2
-rw-r--r--nixos/tests/systemd-boot.nix4
-rw-r--r--nixos/tests/systemd-credentials-tpm2.nix124
-rw-r--r--nixos/tests/systemd-cryptenroll.nix1
-rw-r--r--nixos/tests/systemd-homed.nix99
-rw-r--r--nixos/tests/systemd-initrd-btrfs-raid.nix8
-rw-r--r--nixos/tests/systemd-initrd-luks-fido2.nix8
-rw-r--r--nixos/tests/systemd-initrd-luks-keyfile.nix6
-rw-r--r--nixos/tests/systemd-initrd-luks-password.nix12
-rw-r--r--nixos/tests/systemd-initrd-luks-tpm2.nix8
-rw-r--r--nixos/tests/systemd-initrd-networkd-ssh.nix82
-rw-r--r--nixos/tests/systemd-initrd-networkd.nix74
-rw-r--r--nixos/tests/systemd-initrd-simple.nix6
-rw-r--r--nixos/tests/systemd-initrd-swraid.nix6
-rw-r--r--nixos/tests/systemd-initrd-vconsole.nix33
-rw-r--r--nixos/tests/systemd-repart.nix134
-rw-r--r--nixos/tests/systemd-shutdown.nix1
-rw-r--r--nixos/tests/systemd-user-tmpfiles-rules.nix35
-rw-r--r--nixos/tests/systemd-userdbd.nix32
-rw-r--r--nixos/tests/teleport.nix82
-rw-r--r--nixos/tests/timescaledb.nix93
-rw-r--r--nixos/tests/tracee.nix51
-rw-r--r--nixos/tests/trafficserver.nix1
-rw-r--r--nixos/tests/turbovnc-headless-server.nix2
-rw-r--r--nixos/tests/tuxguitar.nix2
-rw-r--r--nixos/tests/txredisapi.nix2
-rw-r--r--nixos/tests/ulogd.nix84
-rw-r--r--nixos/tests/unifi.nix2
-rw-r--r--nixos/tests/vault-agent.nix52
-rw-r--r--nixos/tests/vaultwarden.nix16
-rw-r--r--nixos/tests/vscodium.nix13
-rw-r--r--nixos/tests/web-apps/mastodon/script.nix2
-rw-r--r--nixos/tests/web-apps/monica.nix33
-rw-r--r--nixos/tests/web-apps/netbox.nix297
-rw-r--r--nixos/tests/web-apps/peertube.nix7
-rw-r--r--nixos/tests/web-apps/snipe-it.nix101
-rw-r--r--nixos/tests/web-servers/stargazer.nix31
-rw-r--r--nixos/tests/webhook.nix65
-rw-r--r--nixos/tests/wireguard/basic.nix3
-rw-r--r--nixos/tests/wireguard/default.nix3
-rw-r--r--nixos/tests/wireguard/generated.nix3
-rw-r--r--nixos/tests/wireguard/namespaces.nix5
-rw-r--r--nixos/tests/wireguard/snakeoil-keys.nix3
-rw-r--r--nixos/tests/wireguard/wg-quick.nix74
-rw-r--r--nixos/tests/wordpress.nix33
-rw-r--r--nixos/tests/xfce.nix2
-rw-r--r--nixos/tests/yggdrasil.nix17
-rw-r--r--nixos/tests/zfs.nix195
-rw-r--r--nixos/tests/zram-generator.nix42
1089 files changed, 36327 insertions, 50431 deletions
diff --git a/nixos/doc/manual/.gitignore b/nixos/doc/manual/.gitignore
deleted file mode 100644
index 87928262421..00000000000
--- a/nixos/doc/manual/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-generated
-manual-combined.xml
diff --git a/nixos/doc/manual/Makefile b/nixos/doc/manual/Makefile
deleted file mode 100644
index b2b6481b20c..00000000000
--- a/nixos/doc/manual/Makefile
+++ /dev/null
@@ -1,30 +0,0 @@
-.PHONY: all
-all: manual-combined.xml
-
-.PHONY: debug
-debug: generated manual-combined.xml
-
-manual-combined.xml: generated *.xml **/*.xml
-	rm -f ./manual-combined.xml
-	nix-shell --pure -Q --packages xmloscopy \
-		--run "xmloscopy --docbook5 ./manual.xml ./manual-combined.xml"
-
-.PHONY: format
-format:
-	nix-shell --pure -Q --packages xmlformat \
-		--run "find ../../ -iname '*.xml' -type f -print0 | xargs -0 -I{} -n1 \
-		xmlformat --config-file '../xmlformat.conf' -i {}"
-
-.PHONY: fix-misc-xml
-fix-misc-xml:
-	find . -iname '*.xml' -type f \
-		-exec ../varlistentry-fixer.rb {} ';'
-
-.PHONY: clean
-clean:
-	rm -f manual-combined.xml generated
-
-generated:
-	nix-build ../../release.nix \
-		--attr manualGeneratedSources.x86_64-linux \
-		--out-link ./generated
diff --git a/nixos/doc/manual/administration/containers.chapter.md b/nixos/doc/manual/administration/containers.chapter.md
index ea51f91f698..50493b562b5 100644
--- a/nixos/doc/manual/administration/containers.chapter.md
+++ b/nixos/doc/manual/administration/containers.chapter.md
@@ -21,8 +21,8 @@ which is often not what you want. By contrast, in the imperative
 approach, containers are configured and updated independently from the
 host system.
 
-```{=docbook}
-<xi:include href="imperative-containers.section.xml" />
-<xi:include href="declarative-containers.section.xml" />
-<xi:include href="container-networking.section.xml" />
+```{=include=} sections
+imperative-containers.section.md
+declarative-containers.section.md
+container-networking.section.md
 ```
diff --git a/nixos/doc/manual/administration/running.md b/nixos/doc/manual/administration/running.md
new file mode 100644
index 00000000000..48e8c7c6668
--- /dev/null
+++ b/nixos/doc/manual/administration/running.md
@@ -0,0 +1,14 @@
+# Administration {#ch-running}
+
+This chapter describes various aspects of managing a running NixOS system, such as how to use the {command}`systemd` service manager.
+
+```{=include=} chapters
+service-mgmt.chapter.md
+rebooting.chapter.md
+user-sessions.chapter.md
+control-groups.chapter.md
+logging.chapter.md
+cleaning-store.chapter.md
+containers.chapter.md
+troubleshooting.chapter.md
+```
diff --git a/nixos/doc/manual/administration/running.xml b/nixos/doc/manual/administration/running.xml
deleted file mode 100644
index d9fcc1aee26..00000000000
--- a/nixos/doc/manual/administration/running.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<part 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="ch-running">
- <title>Administration</title>
- <partintro xml:id="ch-running-intro">
-  <para>
-   This chapter describes various aspects of managing a running NixOS system,
-   such as how to use the <command>systemd</command> service manager.
-  </para>
- </partintro>
- <xi:include href="../from_md/administration/service-mgmt.chapter.xml" />
- <xi:include href="../from_md/administration/rebooting.chapter.xml" />
- <xi:include href="../from_md/administration/user-sessions.chapter.xml" />
- <xi:include href="../from_md/administration/control-groups.chapter.xml" />
- <xi:include href="../from_md/administration/logging.chapter.xml" />
- <xi:include href="../from_md/administration/cleaning-store.chapter.xml" />
- <xi:include href="../from_md/administration/containers.chapter.xml" />
- <xi:include href="../from_md/administration/troubleshooting.chapter.xml" />
-</part>
diff --git a/nixos/doc/manual/administration/service-mgmt.chapter.md b/nixos/doc/manual/administration/service-mgmt.chapter.md
index bb0f9b62e91..674c7374168 100644
--- a/nixos/doc/manual/administration/service-mgmt.chapter.md
+++ b/nixos/doc/manual/administration/service-mgmt.chapter.md
@@ -75,7 +75,7 @@ necessary).
 
 Packages in Nixpkgs sometimes provide systemd units with them, usually
 in e.g `#pkg-out#/lib/systemd/`. Putting such a package in
-`environment.systemPackages` doesn\'t make the service available to
+`environment.systemPackages` doesn't make the service available to
 users or the system.
 
 In order to enable a systemd *system* service with provided upstream
@@ -87,9 +87,9 @@ systemd.packages = [ pkgs.packagekit ];
 
 Usually NixOS modules written by the community do the above, plus take
 care of other details. If a module was written for a service you are
-interested in, you\'d probably need only to use
+interested in, you'd probably need only to use
 `services.#name#.enable = true;`. These services are defined in
-Nixpkgs\' [ `nixos/modules/` directory
+Nixpkgs' [ `nixos/modules/` directory
 ](https://github.com/NixOS/nixpkgs/tree/master/nixos/modules). In case
 the service is simple enough, the above method should work, and start
 the service on boot.
@@ -98,8 +98,8 @@ the service on boot.
 differently. Given a package that has a systemd unit file at
 `#pkg-out#/lib/systemd/user/`, using [](#opt-systemd.packages) will
 make you able to start the service via `systemctl --user start`, but it
-won\'t start automatically on login. However, You can imperatively
-enable it by adding the package\'s attribute to
+won't start automatically on login. However, You can imperatively
+enable it by adding the package's attribute to
 [](#opt-systemd.packages) and then do this (e.g):
 
 ```ShellSession
@@ -113,7 +113,7 @@ If you are interested in a timer file, use `timers.target.wants` instead
 of `default.target.wants` in the 1st and 2nd command.
 
 Using `systemctl --user enable syncthing.service` instead of the above,
-will work, but it\'ll use the absolute path of `syncthing.service` for
+will work, but it'll use the absolute path of `syncthing.service` for
 the symlink, and this path is in `/nix/store/.../lib/systemd/user/`.
 Hence [garbage collection](#sec-nix-gc) will remove that file and you
 will wind up with a broken symlink in your systemd configuration, which
diff --git a/nixos/doc/manual/administration/troubleshooting.chapter.md b/nixos/doc/manual/administration/troubleshooting.chapter.md
index 548456eaf6d..1253607f8ef 100644
--- a/nixos/doc/manual/administration/troubleshooting.chapter.md
+++ b/nixos/doc/manual/administration/troubleshooting.chapter.md
@@ -3,10 +3,10 @@
 This chapter describes solutions to common problems you might encounter
 when you manage your NixOS system.
 
-```{=docbook}
-<xi:include href="boot-problems.section.xml" />
-<xi:include href="maintenance-mode.section.xml" />
-<xi:include href="rollback.section.xml" />
-<xi:include href="store-corruption.section.xml" />
-<xi:include href="network-problems.section.xml" />
+```{=include=} sections
+boot-problems.section.md
+maintenance-mode.section.md
+rollback.section.md
+store-corruption.section.md
+network-problems.section.md
 ```
diff --git a/nixos/doc/manual/configuration/adding-custom-packages.section.md b/nixos/doc/manual/configuration/adding-custom-packages.section.md
index 9219396722f..89d32955061 100644
--- a/nixos/doc/manual/configuration/adding-custom-packages.section.md
+++ b/nixos/doc/manual/configuration/adding-custom-packages.section.md
@@ -94,6 +94,6 @@ 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
+with Nix and special helpers like `autoPatchelfHook` or `buildFHSEnv`. 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/config-file.section.md b/nixos/doc/manual/configuration/config-file.section.md
index efd231fd1f4..b010026c582 100644
--- a/nixos/doc/manual/configuration/config-file.section.md
+++ b/nixos/doc/manual/configuration/config-file.section.md
@@ -170,6 +170,6 @@ Packages
     ```
 
     The latter option definition changes the default PostgreSQL package
-    used by NixOS's PostgreSQL service to 10.x. For more information on
+    used by NixOS's PostgreSQL service to 14.x. For more information on
     packages, including how to add new ones, see
     [](#sec-custom-packages).
diff --git a/nixos/doc/manual/configuration/config-syntax.chapter.md b/nixos/doc/manual/configuration/config-syntax.chapter.md
index 9f8d45d5889..9e606b2b82a 100644
--- a/nixos/doc/manual/configuration/config-syntax.chapter.md
+++ b/nixos/doc/manual/configuration/config-syntax.chapter.md
@@ -11,8 +11,8 @@ manual](https://nixos.org/nix/manual/#chap-writing-nix-expressions), but
 here we give a short overview of the most important constructs useful in
 NixOS configuration files.
 
-```{=docbook}
-<xi:include href="config-file.section.xml" />
-<xi:include href="abstractions.section.xml" />
-<xi:include href="modularity.section.xml" />
+```{=include=} sections
+config-file.section.md
+abstractions.section.md
+modularity.section.md
 ```
diff --git a/nixos/doc/manual/configuration/configuration.md b/nixos/doc/manual/configuration/configuration.md
new file mode 100644
index 00000000000..4c966f3325b
--- /dev/null
+++ b/nixos/doc/manual/configuration/configuration.md
@@ -0,0 +1,27 @@
+# Configuration {#ch-configuration}
+
+This chapter describes how to configure various aspects of a NixOS machine through the configuration file {file}`/etc/nixos/configuration.nix`. As described in [](#sec-changing-config), changes to this file only take effect after you run {command}`nixos-rebuild`.
+
+```{=include=} chapters
+config-syntax.chapter.md
+package-mgmt.chapter.md
+user-mgmt.chapter.md
+file-systems.chapter.md
+x-windows.chapter.md
+wayland.chapter.md
+gpu-accel.chapter.md
+xfce.chapter.md
+networking.chapter.md
+linux-kernel.chapter.md
+subversion.chapter.md
+```
+
+```{=include=} chapters
+@MODULE_CHAPTERS@
+```
+
+```{=include=} chapters
+profiles.chapter.md
+kubernetes.chapter.md
+```
+<!-- Apache; libvirtd virtualisation -->
diff --git a/nixos/doc/manual/configuration/configuration.xml b/nixos/doc/manual/configuration/configuration.xml
deleted file mode 100644
index b04316cfa48..00000000000
--- a/nixos/doc/manual/configuration/configuration.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<part 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="ch-configuration">
- <title>Configuration</title>
- <partintro xml:id="ch-configuration-intro">
-  <para>
-   This chapter describes how to configure various aspects of a NixOS machine
-   through the configuration file
-   <filename>/etc/nixos/configuration.nix</filename>. As described in
-   <xref linkend="sec-changing-config" />, changes to this file only take
-   effect after you run <command>nixos-rebuild</command>.
-  </para>
- </partintro>
- <xi:include href="../from_md/configuration/config-syntax.chapter.xml" />
- <xi:include href="../from_md/configuration/package-mgmt.chapter.xml" />
- <xi:include href="../from_md/configuration/user-mgmt.chapter.xml" />
- <xi:include href="../from_md/configuration/file-systems.chapter.xml" />
- <xi:include href="../from_md/configuration/x-windows.chapter.xml" />
- <xi:include href="../from_md/configuration/wayland.chapter.xml" />
- <xi:include href="../from_md/configuration/gpu-accel.chapter.xml" />
- <xi:include href="../from_md/configuration/xfce.chapter.xml" />
- <xi:include href="../from_md/configuration/networking.chapter.xml" />
- <xi:include href="../from_md/configuration/linux-kernel.chapter.xml" />
- <xi:include href="../from_md/configuration/subversion.chapter.xml" />
- <xi:include href="../generated/modules.xml" xpointer="xpointer(//section[@id='modules']/*)" />
- <xi:include href="../from_md/configuration/profiles.chapter.xml" />
- <xi:include href="../from_md/configuration/kubernetes.chapter.xml" />
-<!-- Apache; libvirtd virtualisation -->
-</part>
diff --git a/nixos/doc/manual/configuration/declarative-packages.section.md b/nixos/doc/manual/configuration/declarative-packages.section.md
index 337cdf8472e..02eaa56192e 100644
--- a/nixos/doc/manual/configuration/declarative-packages.section.md
+++ b/nixos/doc/manual/configuration/declarative-packages.section.md
@@ -40,7 +40,7 @@ configuration use `pkgs` prefix (variable).
 To "uninstall" a package, simply remove it from
 [](#opt-environment.systemPackages) and run `nixos-rebuild switch`.
 
-```{=docbook}
-<xi:include href="customizing-packages.section.xml" />
-<xi:include href="adding-custom-packages.section.xml" />
+```{=include=} sections
+customizing-packages.section.md
+adding-custom-packages.section.md
 ```
diff --git a/nixos/doc/manual/configuration/file-systems.chapter.md b/nixos/doc/manual/configuration/file-systems.chapter.md
index 901e2e4f181..aca978be064 100644
--- a/nixos/doc/manual/configuration/file-systems.chapter.md
+++ b/nixos/doc/manual/configuration/file-systems.chapter.md
@@ -36,7 +36,7 @@ dropping you to the emergency shell. You can make a mount asynchronous
 and non-critical by adding `options = [ "nofail" ];`.
 :::
 
-```{=docbook}
-<xi:include href="luks-file-systems.section.xml" />
-<xi:include href="sshfs-file-systems.section.xml" />
+```{=include=} sections
+luks-file-systems.section.md
+sshfs-file-systems.section.md
 ```
diff --git a/nixos/doc/manual/configuration/kubernetes.chapter.md b/nixos/doc/manual/configuration/kubernetes.chapter.md
index 5d7b083289d..f39726090e4 100644
--- a/nixos/doc/manual/configuration/kubernetes.chapter.md
+++ b/nixos/doc/manual/configuration/kubernetes.chapter.md
@@ -17,7 +17,7 @@ services.kubernetes = {
 };
 ```
 
-Another way is to assign cluster roles (\"master\" and/or \"node\") to
+Another way is to assign cluster roles ("master" and/or "node") to
 the host. This enables apiserver, controllerManager, scheduler,
 addonManager, kube-proxy and etcd:
 
diff --git a/nixos/doc/manual/configuration/linux-kernel.chapter.md b/nixos/doc/manual/configuration/linux-kernel.chapter.md
index 7b84416a864..f5bce99dd1b 100644
--- a/nixos/doc/manual/configuration/linux-kernel.chapter.md
+++ b/nixos/doc/manual/configuration/linux-kernel.chapter.md
@@ -82,61 +82,68 @@ boot.kernel.sysctl."net.ipv4.tcp_keepalive_time" = 120;
 sets the kernel's TCP keepalive time to 120 seconds. To see the
 available parameters, run `sysctl -a`.
 
-## Customize your kernel {#sec-linux-config-customizing}
+## Building a custom kernel {#sec-linux-config-customizing}
 
-The first step before compiling the kernel is to generate an appropriate
-`.config` configuration. Either you pass your own config via the
-`configfile` setting of `linuxKernel.manualConfig`:
+You can customize the default kernel configuration by overriding the arguments for your kernel package:
 
 ```nix
-custom-kernel = let base_kernel = linuxKernel.kernels.linux_4_9;
-  in super.linuxKernel.manualConfig {
-    inherit (super) stdenv hostPlatform;
-    inherit (base_kernel) src;
-    version = "${base_kernel.version}-custom";
-
-    configfile = /home/me/my_kernel_config;
-    allowImportFromDerivation = true;
-};
+pkgs.linux_latest.override {
+  ignoreConfigErrors = true;
+  autoModules = false;
+  kernelPreferBuiltin = true;
+  extraStructuredConfig = with lib.kernel; {
+    DEBUG_KERNEL = yes;
+    FRAME_POINTER = yes;
+    KGDB = yes;
+    KGDB_SERIAL_CONSOLE = yes;
+    DEBUG_INFO = yes;
+  };
+}
 ```
 
-You can edit the config with this snippet (by default `make
-   menuconfig` won\'t work out of the box on nixos):
+See `pkgs/os-specific/linux/kernel/generic.nix` for details on how these arguments
+affect the generated configuration. You can also build a custom version of Linux by calling
+`pkgs.buildLinux` directly, which requires the `src` and `version` arguments to be specified.
 
-```ShellSession
-nix-shell -E 'with import <nixpkgs> {}; kernelToOverride.overrideAttrs (o: {nativeBuildInputs=o.nativeBuildInputs ++ [ pkg-config ncurses ];})'
+To use your custom kernel package in your NixOS configuration, set
+
+```nix
+boot.kernelPackages = pkgs.linuxPackagesFor yourCustomKernel;
 ```
 
-or you can let nixpkgs generate the configuration. Nixpkgs generates it
-via answering the interactive kernel utility `make config`. The answers
-depend on parameters passed to
-`pkgs/os-specific/linux/kernel/generic.nix` (which you can influence by
-overriding `extraConfig, autoModules,
-   modDirVersion, preferBuiltin, extraConfig`).
+Note that this method will use the common configuration defined in `pkgs/os-specific/linux/kernel/common-config.nix`,
+which is suitable for a NixOS system.
+
+If you already have a generated configuration file, you can build a kernel that uses it with `pkgs.linuxManualConfig`:
 
 ```nix
-mptcp93.override ({
-  name="mptcp-local";
+let
+  baseKernel = pkgs.linux_latest;
+in pkgs.linuxManualConfig {
+  inherit (baseKernel) src modDirVersion;
+  version = "${baseKernel.version}-custom";
+  configfile = ./my_kernel_config;
+  allowImportFromDerivation = true;
+}
+```
 
-  ignoreConfigErrors = true;
-  autoModules = false;
-  kernelPreferBuiltin = true;
+::: {.note}
+The build will fail if `modDirVersion` does not match the source's `kernel.release` file,
+so `modDirVersion` should remain tied to `src`.
+:::
 
-  enableParallelBuilding = true;
+To edit the `.config` file for Linux X.Y, proceed as follows:
 
-  extraConfig = ''
-    DEBUG_KERNEL y
-    FRAME_POINTER y
-    KGDB y
-    KGDB_SERIAL_CONSOLE y
-    DEBUG_INFO y
-  '';
-});
+```ShellSession
+$ nix-shell '<nixpkgs>' -A linuxKernel.kernels.linux_X_Y.configEnv
+$ unpackPhase
+$ cd linux-*
+$ make nconfig
 ```
 
 ## Developing kernel modules {#sec-linux-config-developing-modules}
 
-When developing kernel modules it\'s often convenient to run
+When developing kernel modules it's often convenient to run
 edit-compile-run loop as quickly as possible. See below snippet as an
 example of developing `mellanox` drivers.
 
diff --git a/nixos/doc/manual/configuration/modularity.section.md b/nixos/doc/manual/configuration/modularity.section.md
index 3395ace20c4..2eff1538798 100644
--- a/nixos/doc/manual/configuration/modularity.section.md
+++ b/nixos/doc/manual/configuration/modularity.section.md
@@ -67,7 +67,13 @@ When using multiple modules, you may need to access configuration values
 defined in other modules. This is what the `config` function argument is
 for: it contains the complete, merged system configuration. That is,
 `config` is the result of combining the configurations returned by every
-module [^1] . For example, here is a module that adds some packages to
+module. (If you're wondering how it's possible that the (indirect) *result*
+of a function is passed as an *input* to that same function: that's
+because Nix is a "lazy" language --- it only computes values when
+they are needed. This works as long as no individual configuration
+value depends on itself.)
+
+For example, here is a module that adds some packages to
 [](#opt-environment.systemPackages) only if
 [](#opt-services.xserver.enable) is set to `true` somewhere else:
 
@@ -125,9 +131,3 @@ in
 
 { imports = [ (netConfig "nixos.localdomain") ]; }
 ```
-
-[^1]: If you're wondering how it's possible that the (indirect) *result*
-    of a function is passed as an *input* to that same function: that's
-    because Nix is a "lazy" language --- it only computes values when
-    they are needed. This works as long as no individual configuration
-    value depends on itself.
diff --git a/nixos/doc/manual/configuration/networking.chapter.md b/nixos/doc/manual/configuration/networking.chapter.md
index 529dc0610bd..abbd9766f17 100644
--- a/nixos/doc/manual/configuration/networking.chapter.md
+++ b/nixos/doc/manual/configuration/networking.chapter.md
@@ -3,14 +3,14 @@
 This section describes how to configure networking components
 on your NixOS machine.
 
-```{=docbook}
-<xi:include href="network-manager.section.xml" />
-<xi:include href="ssh.section.xml" />
-<xi:include href="ipv4-config.section.xml" />
-<xi:include href="ipv6-config.section.xml" />
-<xi:include href="firewall.section.xml" />
-<xi:include href="wireless.section.xml" />
-<xi:include href="ad-hoc-network-config.section.xml" />
-<xi:include href="renaming-interfaces.section.xml" />
+```{=include=} sections
+network-manager.section.md
+ssh.section.md
+ipv4-config.section.md
+ipv6-config.section.md
+firewall.section.md
+wireless.section.md
+ad-hoc-network-config.section.md
+renaming-interfaces.section.md
 ```
 <!-- TODO: OpenVPN, NAT -->
diff --git a/nixos/doc/manual/configuration/package-mgmt.chapter.md b/nixos/doc/manual/configuration/package-mgmt.chapter.md
index a6c414be59a..1148bbe8474 100644
--- a/nixos/doc/manual/configuration/package-mgmt.chapter.md
+++ b/nixos/doc/manual/configuration/package-mgmt.chapter.md
@@ -12,7 +12,7 @@ NixOS has two distinct styles of package management:
     `nix-env` command. This style allows mixing packages from different
     Nixpkgs versions. It's the only choice for non-root users.
 
-```{=docbook}
-<xi:include href="declarative-packages.section.xml" />
-<xi:include href="ad-hoc-packages.section.xml" />
+```{=include=} sections
+declarative-packages.section.md
+ad-hoc-packages.section.md
 ```
diff --git a/nixos/doc/manual/configuration/profiles.chapter.md b/nixos/doc/manual/configuration/profiles.chapter.md
index b4ae1b7d3fa..9f1f48f742a 100644
--- a/nixos/doc/manual/configuration/profiles.chapter.md
+++ b/nixos/doc/manual/configuration/profiles.chapter.md
@@ -2,7 +2,7 @@
 
 In some cases, it may be desirable to take advantage of commonly-used,
 predefined configurations provided by nixpkgs, but different from those
-that come as default. This is a role fulfilled by NixOS\'s Profiles,
+that come as default. This is a role fulfilled by NixOS's Profiles,
 which come as files living in `<nixpkgs/nixos/modules/profiles>`. That
 is to say, expected usage is to add them to the imports list of your
 `/etc/configuration.nix` as such:
@@ -19,16 +19,16 @@ install media, many are actually intended to be used in real installs.
 What follows is a brief explanation on the purpose and use-case for each
 profile. Detailing each option configured by each one is out of scope.
 
-```{=docbook}
-<xi:include href="profiles/all-hardware.section.xml" />
-<xi:include href="profiles/base.section.xml" />
-<xi:include href="profiles/clone-config.section.xml" />
-<xi:include href="profiles/demo.section.xml" />
-<xi:include href="profiles/docker-container.section.xml" />
-<xi:include href="profiles/graphical.section.xml" />
-<xi:include href="profiles/hardened.section.xml" />
-<xi:include href="profiles/headless.section.xml" />
-<xi:include href="profiles/installation-device.section.xml" />
-<xi:include href="profiles/minimal.section.xml" />
-<xi:include href="profiles/qemu-guest.section.xml" />
+```{=include=} sections
+profiles/all-hardware.section.md
+profiles/base.section.md
+profiles/clone-config.section.md
+profiles/demo.section.md
+profiles/docker-container.section.md
+profiles/graphical.section.md
+profiles/hardened.section.md
+profiles/headless.section.md
+profiles/installation-device.section.md
+profiles/minimal.section.md
+profiles/qemu-guest.section.md
 ```
diff --git a/nixos/doc/manual/configuration/ssh.section.md b/nixos/doc/manual/configuration/ssh.section.md
index cba81eb43f4..9e239a84817 100644
--- a/nixos/doc/manual/configuration/ssh.section.md
+++ b/nixos/doc/manual/configuration/ssh.section.md
@@ -8,7 +8,7 @@ services.openssh.enable = true;
 
 By default, root logins using a password are disallowed. They can be
 disabled entirely by setting
-[](#opt-services.openssh.permitRootLogin) to `"no"`.
+[](#opt-services.openssh.settings.PermitRootLogin) to `"no"`.
 
 You can declaratively specify authorised RSA/DSA public keys for a user
 as follows:
diff --git a/nixos/doc/manual/configuration/sshfs-file-systems.section.md b/nixos/doc/manual/configuration/sshfs-file-systems.section.md
index 4dd1b203a24..d8c9dea6c33 100644
--- a/nixos/doc/manual/configuration/sshfs-file-systems.section.md
+++ b/nixos/doc/manual/configuration/sshfs-file-systems.section.md
@@ -8,7 +8,7 @@ It means that if you have SSH access to a machine, no additional setup is needed
 
 ## Interactive mounting {#sec-sshfs-interactive}
 
-In NixOS, SSHFS is packaged as <package>sshfs</package>.
+In NixOS, SSHFS is packaged as `sshfs`.
 Once installed, mounting a directory interactively is simple as running:
 ```ShellSession
 $ sshfs my-user@example.com:/my-dir /mnt/my-dir
diff --git a/nixos/doc/manual/configuration/user-mgmt.chapter.md b/nixos/doc/manual/configuration/user-mgmt.chapter.md
index 5c3aca3ef9e..b35b38f6e96 100644
--- a/nixos/doc/manual/configuration/user-mgmt.chapter.md
+++ b/nixos/doc/manual/configuration/user-mgmt.chapter.md
@@ -30,7 +30,7 @@ to your NixOS configuration. For instance, if you remove a user from
 [](#opt-users.users) and run nixos-rebuild, the user
 account will cease to exist. Also, imperative commands for managing users and
 groups, such as useradd, are no longer available. Passwords may still be
-assigned by setting the user\'s
+assigned by setting the user's
 [hashedPassword](#opt-users.users._name_.hashedPassword) option. A
 hashed password can be generated using `mkpasswd`.
 
diff --git a/nixos/doc/manual/configuration/wayland.chapter.md b/nixos/doc/manual/configuration/wayland.chapter.md
index a3a46aa3da6..0f195bd6656 100644
--- a/nixos/doc/manual/configuration/wayland.chapter.md
+++ b/nixos/doc/manual/configuration/wayland.chapter.md
@@ -4,7 +4,7 @@ While X11 (see [](#sec-x11)) is still the primary display technology
 on NixOS, Wayland support is steadily improving. Where X11 separates the
 X Server and the window manager, on Wayland those are combined: a
 Wayland Compositor is like an X11 window manager, but also embeds the
-Wayland \'Server\' functionality. This means it is sufficient to install
+Wayland 'Server' functionality. This means it is sufficient to install
 a Wayland Compositor such as sway without separately enabling a Wayland
 server:
 
diff --git a/nixos/doc/manual/configuration/x-windows.chapter.md b/nixos/doc/manual/configuration/x-windows.chapter.md
index 27d11723880..bef35f44887 100644
--- a/nixos/doc/manual/configuration/x-windows.chapter.md
+++ b/nixos/doc/manual/configuration/x-windows.chapter.md
@@ -69,7 +69,7 @@ Wine, you should also set the following:
 hardware.opengl.driSupport32Bit = true;
 ```
 
-## Auto-login {#sec-x11-auto-login .unnumbered}
+## Auto-login {#sec-x11-auto-login}
 
 The x11 login screen can be skipped entirely, automatically logging you
 into your window manager and desktop environment when you boot your
@@ -81,7 +81,7 @@ second password to login can be redundant.
 
 To enable auto-login, you need to define your default window manager and
 desktop environment. If you wanted no desktop environment and i3 as your
-your window manager, you\'d define:
+your window manager, you'd define:
 
 ```nix
 services.xserver.displayManager.defaultSession = "none+i3";
@@ -96,7 +96,7 @@ services.xserver.displayManager.autoLogin.enable = true;
 services.xserver.displayManager.autoLogin.user = "alice";
 ```
 
-## Intel Graphics drivers {#sec-x11--graphics-cards-intel .unnumbered}
+## Intel Graphics drivers {#sec-x11--graphics-cards-intel}
 
 There are two choices for Intel Graphics drivers in X.org: `modesetting`
 (included in the xorg-server itself) and `intel` (provided by the
@@ -110,7 +110,7 @@ maintained but may perform worse in some cases (like in old chipsets).
 
 The second driver, `intel`, is specific to Intel GPUs, but not
 recommended by most distributions: it lacks several modern features (for
-example, it doesn\'t support Glamor) and the package hasn\'t been
+example, it doesn't support Glamor) and the package hasn't been
 officially updated since 2015.
 
 The results vary depending on the hardware, so you may have to try both
@@ -136,7 +136,7 @@ services.xserver.deviceSection = ''
 Note that this will likely downgrade the performance compared to
 `modesetting` or `intel` with DRI 3 (default).
 
-## Proprietary NVIDIA drivers {#sec-x11-graphics-cards-nvidia .unnumbered}
+## Proprietary NVIDIA drivers {#sec-x11-graphics-cards-nvidia}
 
 NVIDIA provides a proprietary driver for its graphics cards that has
 better 3D performance than the X.org drivers. It is not enabled by
@@ -158,11 +158,11 @@ services.xserver.videoDrivers = [ "nvidiaLegacy304" ];
 You may need to reboot after enabling this driver to prevent a clash
 with other kernel modules.
 
-## Proprietary AMD drivers {#sec-x11--graphics-cards-amd .unnumbered}
+## Proprietary AMD drivers {#sec-x11--graphics-cards-amd}
 
 AMD provides a proprietary driver for its graphics cards that is not
 enabled by default because it's not Free Software, is often broken in
-nixpkgs and as of this writing doesn\'t offer more features or
+nixpkgs and as of this writing doesn't offer more features or
 performance. If you still want to use it anyway, you need to explicitly
 set:
 
@@ -173,7 +173,7 @@ services.xserver.videoDrivers = [ "amdgpu-pro" ];
 You will need to reboot after enabling this driver to prevent a clash
 with other kernel modules.
 
-## Touchpads {#sec-x11-touchpads .unnumbered}
+## Touchpads {#sec-x11-touchpads}
 
 Support for Synaptics touchpads (found in many laptops such as the Dell
 Latitude series) can be enabled as follows:
@@ -192,19 +192,19 @@ services.xserver.libinput.touchpad.tapping = false;
 Note: the use of `services.xserver.synaptics` is deprecated since NixOS
 17.09.
 
-## GTK/Qt themes {#sec-x11-gtk-and-qt-themes .unnumbered}
+## GTK/Qt themes {#sec-x11-gtk-and-qt-themes}
 
 GTK themes can be installed either to user profile or system-wide (via
 `environment.systemPackages`). To make Qt 5 applications look similar to
 GTK ones, you can use the following configuration:
 
 ```nix
-qt5.enable = true;
-qt5.platformTheme = "gtk2";
-qt5.style = "gtk2";
+qt.enable = true;
+qt.platformTheme = "gtk2";
+qt.style = "gtk2";
 ```
 
-## Custom XKB layouts {#custom-xkb-layouts .unnumbered}
+## Custom XKB layouts {#custom-xkb-layouts}
 
 It is possible to install custom [ XKB
 ](https://en.wikipedia.org/wiki/X_keyboard_extension) keyboard layouts
@@ -215,7 +215,7 @@ US layout, with an additional layer to type some greek symbols by
 pressing the right-alt key.
 
 Create a file called `us-greek` with the following content (under a
-directory called `symbols`; it\'s an XKB peculiarity that will help with
+directory called `symbols`; it's an XKB peculiarity that will help with
 testing):
 
 ```nix
@@ -249,7 +249,7 @@ The name (after `extraLayouts.`) should match the one given to the
 
 Applying this customization requires rebuilding several packages, and a
 broken XKB file can lead to the X session crashing at login. Therefore,
-you\'re strongly advised to **test your layout before applying it**:
+you're strongly advised to **test your layout before applying it**:
 
 ```ShellSession
 $ nix-shell -p xorg.xkbcomp
@@ -313,8 +313,8 @@ prefer to keep the layout definitions inside the NixOS configuration.
 
 Unfortunately, the Xorg server does not (currently) support setting a
 keymap directly but relies instead on XKB rules to select the matching
-components (keycodes, types, \...) of a layout. This means that
-components other than symbols won\'t be loaded by default. As a
+components (keycodes, types, ...) of a layout. This means that
+components other than symbols won't be loaded by default. As a
 workaround, you can set the keymap using `setxkbmap` at the start of the
 session with:
 
@@ -323,7 +323,7 @@ services.xserver.displayManager.sessionCommands = "setxkbmap -keycodes media";
 ```
 
 If you are manually starting the X server, you should set the argument
-`-xkbdir /etc/X11/xkb`, otherwise X won\'t find your layout files. For
+`-xkbdir /etc/X11/xkb`, otherwise X won't find your layout files. For
 example with `xinit` run
 
 ```ShellSession
diff --git a/nixos/doc/manual/configuration/xfce.chapter.md b/nixos/doc/manual/configuration/xfce.chapter.md
index ee60d465e3b..a80be2b523e 100644
--- a/nixos/doc/manual/configuration/xfce.chapter.md
+++ b/nixos/doc/manual/configuration/xfce.chapter.md
@@ -24,18 +24,18 @@ Some Xfce programs are not installed automatically. To install them
 manually (system wide), put them into your
 [](#opt-environment.systemPackages) from `pkgs.xfce`.
 
-## Thunar {#sec-xfce-thunar-plugins .unnumbered}
+## Thunar {#sec-xfce-thunar-plugins}
 
 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-programs.thunar.plugins). You shouldn\'t just add them to
+If you'd like to add extra plugins to Thunar, add them to
+[](#opt-programs.thunar.plugins). You shouldn't just add them to
 [](#opt-environment.systemPackages).
 
-## Troubleshooting {#sec-xfce-troubleshooting .unnumbered}
+## Troubleshooting {#sec-xfce-troubleshooting}
 
 Even after enabling udisks2, volume management might not work. Thunar
 and/or the desktop takes time to show up. Thunar will spit out this kind
@@ -46,7 +46,7 @@ Thunar:2410): GVFS-RemoteVolumeMonitor-WARNING **: remote volume monitor with db
 ```
 
 This is caused by some needed GNOME services not running. This is all
-fixed by enabling \"Launch GNOME services on startup\" in the Advanced
+fixed by enabling "Launch GNOME services on startup" in the Advanced
 tab of the Session and Startup settings panel. Alternatively, you can
 run this command to do the same thing.
 
@@ -54,4 +54,4 @@ run this command to do the same thing.
 $ xfconf-query -c xfce4-session -p /compat/LaunchGNOME -s true
 ```
 
-A log-out and re-log will be needed for this to take effect.
+It is necessary to log out and log in again for this to take effect.
diff --git a/nixos/doc/manual/contributing-to-this-manual.chapter.md b/nixos/doc/manual/contributing-to-this-manual.chapter.md
index 55759980922..c306cc084cd 100644
--- a/nixos/doc/manual/contributing-to-this-manual.chapter.md
+++ b/nixos/doc/manual/contributing-to-this-manual.chapter.md
@@ -6,7 +6,6 @@ You can quickly check your edits with the following:
 
 ```ShellSession
 $ cd /path/to/nixpkgs
-$ ./nixos/doc/manual/md-to-db.sh
 $ nix-build nixos/release.nix -A manual.x86_64-linux
 ```
 
diff --git a/nixos/doc/manual/default.nix b/nixos/doc/manual/default.nix
index 9b72e840f4b..68132f302e4 100644
--- a/nixos/doc/manual/default.nix
+++ b/nixos/doc/manual/default.nix
@@ -21,6 +21,8 @@ let
     withManOptDedupPatch = true;
   };
 
+  manpageUrls = pkgs.path + "/doc/manpage-urls.json";
+
   # 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.
@@ -48,7 +50,7 @@ let
       };
     in buildPackages.nixosOptionsDoc {
       inherit (eval) options;
-      inherit (revision);
+      inherit revision;
       transformOptions = opt: opt // {
         # Clean up declaration sites to not refer to the NixOS source tree.
         declarations =
@@ -66,31 +68,6 @@ let
       optionIdPrefix = "test-opt-";
     };
 
-  sources = lib.sourceFilesBySuffices ./. [".xml"];
-
-  modulesDoc = builtins.toFile "modules.xml" ''
-    <section xmlns:xi="http://www.w3.org/2001/XInclude" id="modules">
-    ${(lib.concatMapStrings (path: ''
-      <xi:include href="${path}" />
-    '') (lib.catAttrs "value" config.meta.doc))}
-    </section>
-  '';
-
-  generatedSources = runCommand "generated-docbook" {} ''
-    mkdir $out
-    ln -s ${modulesDoc} $out/modules.xml
-    ln -s ${optionsDoc.optionsDocBook} $out/options-db.xml
-    ln -s ${testOptionsDoc.optionsDocBook} $out/test-options-db.xml
-    printf "%s" "${version}" > $out/version
-  '';
-
-  copySources =
-    ''
-      cp -prd $sources/* . # */
-      ln -s ${generatedSources} ./generated
-      chmod -R u+w .
-    '';
-
   toc = builtins.toFile "toc.xml"
     ''
       <toc role="chunk-toc">
@@ -121,100 +98,117 @@ let
     "--stringparam chunk.toc ${toc}"
   ];
 
+  linterFunctions = ''
+    # outputs the context of an xmllint error output
+    # LEN lines around the failing line are printed
+    function context {
+      # length of context
+      local LEN=6
+      # lines to print before error line
+      local BEFORE=4
+
+      # xmllint output lines are:
+      # file.xml:1234: there was an error on line 1234
+      while IFS=':' read -r file line rest; do
+        echo
+        if [[ -n "$rest" ]]; then
+          echo "$file:$line:$rest"
+          local FROM=$(($line>$BEFORE ? $line - $BEFORE : 1))
+          # number lines & filter context
+          nl --body-numbering=a "$file" | sed -n "$FROM,+$LEN p"
+        else
+          if [[ -n "$line" ]]; then
+            echo "$file:$line"
+          else
+            echo "$file"
+          fi
+        fi
+      done
+    }
+
+    function lintrng {
+      xmllint --debug --noout --nonet \
+        --relaxng ${docbook5}/xml/rng/docbook/docbook.rng \
+        "$1" \
+        2>&1 | context 1>&2
+        # ^ redirect assumes xmllint doesn’t print to stdout
+    }
+  '';
+
+  prepareManualFromMD = ''
+    cp -r --no-preserve=all $inputs/* .
+
+    substituteInPlace ./manual.md \
+      --replace '@NIXOS_VERSION@' "${version}"
+    substituteInPlace ./configuration/configuration.md \
+      --replace \
+          '@MODULE_CHAPTERS@' \
+          ${lib.escapeShellArg (lib.concatMapStringsSep "\n" (p: "${p.value}") config.meta.doc)}
+    substituteInPlace ./nixos-options.md \
+      --replace \
+        '@NIXOS_OPTIONS_JSON@' \
+        ${optionsDoc.optionsJSON}/share/doc/nixos/options.json
+    substituteInPlace ./development/writing-nixos-tests.section.md \
+      --replace \
+        '@NIXOS_TEST_OPTIONS_JSON@' \
+        ${testOptionsDoc.optionsJSON}/share/doc/nixos/options.json
+  '';
+
   manual-combined = runCommand "nixos-manual-combined"
-    { inherit sources;
-      nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin ];
+    { inputs = lib.sourceFilesBySuffices ./. [ ".xml" ".md" ];
+      nativeBuildInputs = [ pkgs.nixos-render-docs pkgs.libxml2.bin pkgs.libxslt.bin ];
       meta.description = "The NixOS manual as plain docbook XML";
     }
     ''
-      ${copySources}
-
-      xmllint --xinclude --output ./manual-combined.xml ./manual.xml
-      xmllint --xinclude --noxincludenode \
-         --output ./man-pages-combined.xml ./man-pages.xml
-
-      # outputs the context of an xmllint error output
-      # LEN lines around the failing line are printed
-      function context {
-        # length of context
-        local LEN=6
-        # lines to print before error line
-        local BEFORE=4
-
-        # xmllint output lines are:
-        # file.xml:1234: there was an error on line 1234
-        while IFS=':' read -r file line rest; do
-          echo
-          if [[ -n "$rest" ]]; then
-            echo "$file:$line:$rest"
-            local FROM=$(($line>$BEFORE ? $line - $BEFORE : 1))
-            # number lines & filter context
-            nl --body-numbering=a "$file" | sed -n "$FROM,+$LEN p"
-          else
-            if [[ -n "$line" ]]; then
-              echo "$file:$line"
-            else
-              echo "$file"
-            fi
-          fi
-        done
-      }
+      ${prepareManualFromMD}
 
-      function lintrng {
-        xmllint --debug --noout --nonet \
-          --relaxng ${docbook5}/xml/rng/docbook/docbook.rng \
-          "$1" \
-          2>&1 | context 1>&2
-          # ^ redirect assumes xmllint doesn’t print to stdout
-      }
+      nixos-render-docs -j $NIX_BUILD_CORES manual docbook \
+        --manpage-urls ${manpageUrls} \
+        --revision ${lib.escapeShellArg revision} \
+        ./manual.md \
+        ./manual-combined-pre.xml
+
+      xsltproc \
+        -o manual-combined.xml ${./../../lib/make-options-doc/postprocess-option-descriptions.xsl} \
+        manual-combined-pre.xml
+
+      ${linterFunctions}
 
       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"
-    { inherit sources;
-      nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin ];
+  manpages-combined = runCommand "nixos-manpages-combined.xml"
+    { nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin ];
+      meta.description = "The NixOS manpages as plain docbook XML";
     }
     ''
-      xsltproc \
-        ${manualXsltprocOptions} \
-        --stringparam collect.xref.targets only \
-        --stringparam targets.filename "$out/manual.db" \
-        --nonet \
-        ${docbook_xsl_ns}/xml/xsl/docbook/xhtml/chunktoc.xsl \
-        ${manual-combined}/manual-combined.xml
+      mkdir generated
+      cp -prd ${./man-pages.xml} man-pages.xml
+      ln -s ${optionsDoc.optionsDocBook} generated/options-db.xml
+
+      xmllint --xinclude --noxincludenode --output $out ./man-pages.xml
+
+      ${linterFunctions}
 
-      cat > "$out/olinkdb.xml" <<EOF
-      <?xml version="1.0" encoding="utf-8"?>
-      <!DOCTYPE targetset SYSTEM
-        "file://${docbook_xsl_ns}/xml/xsl/docbook/common/targetdatabase.dtd" [
-        <!ENTITY manualtargets SYSTEM "file://$out/manual.db">
-      ]>
-      <targetset>
-        <targetsetinfo>
-            Allows for cross-referencing olinks between the manpages
-            and manual.
-        </targetsetinfo>
-
-        <document targetdoc="manual">&manualtargets;</document>
-      </targetset>
-      EOF
+      lintrng $out
     '';
 
 in rec {
-  inherit generatedSources;
-
-  inherit (optionsDoc) optionsJSON optionsNix optionsDocBook;
+  inherit (optionsDoc) optionsJSON optionsNix optionsDocBook optionsUsedDocbook;
 
   # Generate the NixOS manual.
   manualHTML = runCommand "nixos-manual-html"
-    { inherit sources;
-      nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin ];
+    { nativeBuildInputs =
+        if allowDocBook then [
+          buildPackages.libxml2.bin
+          buildPackages.libxslt.bin
+        ] else [
+          buildPackages.nixos-render-docs
+        ];
+      inputs = lib.optionals (! allowDocBook) (lib.sourceFilesBySuffices ./. [ ".md" ]);
       meta.description = "The NixOS manual in HTML format";
       allowedReferences = ["out"];
     }
@@ -222,24 +216,44 @@ in rec {
       # Generate the HTML manual.
       dst=$out/share/doc/nixos
       mkdir -p $dst
-      xsltproc \
-        ${manualXsltprocOptions} \
-        --stringparam target.database.document "${olinkDB}/olinkdb.xml" \
-        --stringparam id.warnings "1" \
-        --nonet --output $dst/ \
-        ${docbook_xsl_ns}/xml/xsl/docbook/xhtml/chunktoc.xsl \
-        ${manual-combined}/manual-combined.xml \
-        |& tee xsltproc.out
-      grep "^ID recommended on" xsltproc.out &>/dev/null && echo "error: some IDs are missing" && false
-      rm xsltproc.out
-
-      mkdir -p $dst/images/callouts
-      cp ${docbook_xsl_ns}/xml/xsl/docbook/images/callouts/*.svg $dst/images/callouts/
 
       cp ${../../../doc/style.css} $dst/style.css
       cp ${../../../doc/overrides.css} $dst/overrides.css
       cp -r ${pkgs.documentation-highlighter} $dst/highlightjs
 
+      ${if allowDocBook then ''
+          xsltproc \
+            ${manualXsltprocOptions} \
+            --stringparam id.warnings "1" \
+            --nonet --output $dst/ \
+            ${docbook_xsl_ns}/xml/xsl/docbook/xhtml/chunktoc.xsl \
+            ${manual-combined}/manual-combined.xml \
+            |& tee xsltproc.out
+          grep "^ID recommended on" xsltproc.out &>/dev/null && echo "error: some IDs are missing" && false
+          rm xsltproc.out
+
+          mkdir -p $dst/images/callouts
+          cp ${docbook_xsl_ns}/xml/xsl/docbook/images/callouts/*.svg $dst/images/callouts/
+        '' else ''
+          ${prepareManualFromMD}
+
+          # TODO generator is set like this because the docbook/md manual compare workflow will
+          # trigger if it's different
+          nixos-render-docs -j $NIX_BUILD_CORES manual html \
+            --manpage-urls ${manpageUrls} \
+            --revision ${lib.escapeShellArg revision} \
+            --generator "DocBook XSL Stylesheets V${docbook_xsl_ns.version}" \
+            --stylesheet style.css \
+            --stylesheet overrides.css \
+            --stylesheet highlightjs/mono-blue.css \
+            --script ./highlightjs/highlight.pack.js \
+            --script ./highlightjs/loader.js \
+            --toc-depth 1 \
+            --chunk-toc-depth 1 \
+            ./manual.md \
+            $dst/index.html
+        ''}
+
       mkdir -p $out/nix-support
       echo "nix-build out $out" >> $out/nix-support/hydra-build-products
       echo "doc manual $dst" >> $out/nix-support/hydra-build-products
@@ -252,8 +266,7 @@ in rec {
   manualHTMLIndex = "${manualHTML}/share/doc/nixos/index.html";
 
   manualEpub = runCommand "nixos-manual-epub"
-    { inherit sources;
-      nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin buildPackages.zip ];
+    { nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin buildPackages.zip ];
     }
     ''
       # Generate the epub manual.
@@ -261,7 +274,6 @@ in rec {
 
       xsltproc \
         ${manualXsltprocOptions} \
-        --stringparam target.database.document "${olinkDB}/olinkdb.xml" \
         --nonet --xinclude --output $dst/epub/ \
         ${docbook_xsl_ns}/xml/xsl/docbook/epub/docbook.xsl \
         ${manual-combined}/manual-combined.xml
@@ -282,22 +294,38 @@ in rec {
 
   # Generate the NixOS manpages.
   manpages = runCommand "nixos-manpages"
-    { inherit sources;
-      nativeBuildInputs = [ buildPackages.libxml2.bin buildPackages.libxslt.bin ];
+    { nativeBuildInputs = [
+        buildPackages.installShellFiles
+      ] ++ lib.optionals allowDocBook [
+        buildPackages.libxml2.bin
+        buildPackages.libxslt.bin
+      ] ++ lib.optionals (! allowDocBook) [
+        buildPackages.nixos-render-docs
+      ];
       allowedReferences = ["out"];
     }
     ''
       # Generate manpages.
-      mkdir -p $out/share/man
-      xsltproc --nonet \
-        --maxdepth 6000 \
-        --param man.output.in.separate.dir 1 \
-        --param man.output.base.dir "'$out/share/man/'" \
-        --param man.endnotes.are.numbered 0 \
-        --param man.break.after.slash 1 \
-        --stringparam target.database.document "${olinkDB}/olinkdb.xml" \
-        ${docbook_xsl_ns}/xml/xsl/docbook/manpages/docbook.xsl \
-        ${manual-combined}/man-pages-combined.xml
+      mkdir -p $out/share/man/man8
+      installManPage ${./manpages}/*
+      ${if allowDocBook
+        then ''
+          xsltproc --nonet \
+            --maxdepth 6000 \
+            --param man.output.in.separate.dir 1 \
+            --param man.output.base.dir "'$out/share/man/'" \
+            --param man.endnotes.are.numbered 0 \
+            --param man.break.after.slash 1 \
+            ${docbook_xsl_ns}/xml/xsl/docbook/manpages/docbook.xsl \
+            ${manpages-combined}
+        ''
+        else ''
+          mkdir -p $out/share/man/man5
+          nixos-render-docs -j $NIX_BUILD_CORES options manpage \
+            --revision ${lib.escapeShellArg revision} \
+            ${optionsJSON}/share/doc/nixos/options.json \
+            $out/share/man/man5/configuration.nix.5
+        ''}
     '';
 
 }
diff --git a/nixos/doc/manual/development/developing-the-test-driver.chapter.md b/nixos/doc/manual/development/developing-the-test-driver.chapter.md
new file mode 100644
index 00000000000..d64574fa62a
--- /dev/null
+++ b/nixos/doc/manual/development/developing-the-test-driver.chapter.md
@@ -0,0 +1,45 @@
+
+# Developing the NixOS Test Driver {#chap-developing-the-test-driver}
+
+The NixOS test framework is a project of its own.
+
+It consists of roughly the following components:
+
+ - `nixos/lib/test-driver`: The Python framework that sets up the test and runs the [`testScript`](#test-opt-testScript)
+ - `nixos/lib/testing`: The Nix code responsible for the wiring, written using the (NixOS) Module System.
+
+These components are exposed publicly through:
+
+ - `nixos/lib/default.nix`: The public interface that exposes the `nixos/lib/testing` entrypoint.
+ - `flake.nix`: Exposes the `lib.nixos`, including the public test interface.
+
+Beyond the test driver itself, its integration into NixOS and Nixpkgs is important.
+
+ - `pkgs/top-level/all-packages.nix`: Defines the `nixosTests` attribute, used
+   by the package `tests` attributes and OfBorg.
+ - `nixos/release.nix`: Defines the `tests` attribute built by Hydra, independently, but analogous to `nixosTests`
+ - `nixos/release-combined.nix`: Defines which tests are channel blockers.
+
+Finally, we have legacy entrypoints that users should move away from, but are cared for on a best effort basis.
+These include `pkgs.nixosTest`, `testing-python.nix` and `make-test-python.nix`.
+
+## Testing changes to the test framework {#sec-test-the-test-framework}
+
+We currently have limited unit tests for the framework itself. You may run these with `nix-build -A nixosTests.nixos-test-driver`.
+
+When making significant changes to the test framework, we run the tests on Hydra, to avoid disrupting the larger NixOS project.
+
+For this, we use the `python-test-refactoring` branch in the `NixOS/nixpkgs` repository, and its [corresponding Hydra jobset](https://hydra.nixos.org/jobset/nixos/python-test-refactoring).
+This branch is used as a pointer, and not as a feature branch.
+
+1. Rebase the PR onto a recent, good evaluation of `nixos-unstable`
+2. Create a baseline evaluation by force-pushing this revision of `nixos-unstable` to `python-test-refactoring`.
+3. Note the evaluation number (we'll call it `<previous>`)
+4. Push the PR to `python-test-refactoring` and evaluate the PR on Hydra
+5. Create a comparison URL by navigating to the latest build of the PR and adding to the URL `?compare=<previous>`. This is not necessary for the evaluation that comes right after the baseline.
+
+Review the removed tests and newly failed tests using the constructed URL; otherwise you will accidentally compare iterations of the PR instead of changes to the PR base.
+
+As we currently have some flaky tests, newly failing tests are expected, but should be reviewed to make sure that
+ - The number of failures did not increase significantly.
+ - All failures that do occur can reasonably be assumed to fail for a different reason than the changes.
diff --git a/nixos/doc/manual/development/development.md b/nixos/doc/manual/development/development.md
new file mode 100644
index 00000000000..76f405c3b29
--- /dev/null
+++ b/nixos/doc/manual/development/development.md
@@ -0,0 +1,15 @@
+# Development {#ch-development}
+
+This chapter describes how you can modify and extend NixOS.
+
+```{=include=} chapters
+sources.chapter.md
+writing-modules.chapter.md
+building-parts.chapter.md
+bootspec.chapter.md
+what-happens-during-a-system-switch.chapter.md
+writing-documentation.chapter.md
+nixos-tests.chapter.md
+developing-the-test-driver.chapter.md
+testing-installer.chapter.md
+```
diff --git a/nixos/doc/manual/development/development.xml b/nixos/doc/manual/development/development.xml
deleted file mode 100644
index 949468c9021..00000000000
--- a/nixos/doc/manual/development/development.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<part   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="ch-development">
- <title>Development</title>
- <partintro xml:id="ch-development-intro">
-  <para>
-   This chapter describes how you can modify and extend NixOS.
-  </para>
- </partintro>
- <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/bootspec.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/nixos-tests.chapter.xml" />
- <xi:include href="../from_md/development/testing-installer.chapter.xml" />
-</part>
diff --git a/nixos/doc/manual/development/freeform-modules.section.md b/nixos/doc/manual/development/freeform-modules.section.md
index 10e876b96d5..4f344dd8046 100644
--- a/nixos/doc/manual/development/freeform-modules.section.md
+++ b/nixos/doc/manual/development/freeform-modules.section.md
@@ -13,9 +13,8 @@ checking for entire option trees, it is only recommended for use in
 submodules.
 
 ::: {#ex-freeform-module .example}
-::: {.title}
-**Example: Freeform submodule**
-:::
+### Freeform submodule
+
 The following shows a submodule assigning a freeform type that allows
 arbitrary attributes with `str` values below `settings`, but also
 declares an option for the `settings.port` attribute to have it
diff --git a/nixos/doc/manual/development/meta-attributes.section.md b/nixos/doc/manual/development/meta-attributes.section.md
index 946c08efd0a..33b41fe74d2 100644
--- a/nixos/doc/manual/development/meta-attributes.section.md
+++ b/nixos/doc/manual/development/meta-attributes.section.md
@@ -23,7 +23,7 @@ file.
 
   meta = {
     maintainers = with lib.maintainers; [ ericsagnes ];
-    doc = ./default.xml;
+    doc = ./default.md;
     buildDocsInSandbox = true;
   };
 }
@@ -31,7 +31,9 @@ file.
 
 -   `maintainers` contains a list of the module maintainers.
 
--   `doc` points to a valid DocBook file containing the module
+-   `doc` points to a valid [Nixpkgs-flavored CommonMark](
+      https://nixos.org/manual/nixpkgs/unstable/#sec-contributing-markup
+    ) file containing the module
     documentation. Its contents is automatically added to
     [](#ch-configuration). Changes to a module documentation have to
     be checked to not break building the NixOS manual:
diff --git a/nixos/doc/manual/development/nixos-tests.chapter.md b/nixos/doc/manual/development/nixos-tests.chapter.md
index 2a4fdddeaa6..ec0e4b9f076 100644
--- a/nixos/doc/manual/development/nixos-tests.chapter.md
+++ b/nixos/doc/manual/development/nixos-tests.chapter.md
@@ -5,9 +5,9 @@ NixOS tests are kept in the directory `nixos/tests`, and are executed
 (using Nix) by a testing framework that automatically starts one or more
 virtual machines containing the NixOS system(s) required for the test.
 
-```{=docbook}
-<xi:include href="writing-nixos-tests.section.xml" />
-<xi:include href="running-nixos-tests.section.xml" />
-<xi:include href="running-nixos-tests-interactively.section.xml" />
-<xi:include href="linking-nixos-tests-to-packages.section.xml" />
+```{=include=} sections
+writing-nixos-tests.section.md
+running-nixos-tests.section.md
+running-nixos-tests-interactively.section.md
+linking-nixos-tests-to-packages.section.md
 ```
diff --git a/nixos/doc/manual/development/option-declarations.section.md b/nixos/doc/manual/development/option-declarations.section.md
index 88617ab1920..3448b07722b 100644
--- a/nixos/doc/manual/development/option-declarations.section.md
+++ b/nixos/doc/manual/development/option-declarations.section.md
@@ -77,8 +77,9 @@ The option's description is "Whether to enable \<name\>.".
 For example:
 
 ::: {#ex-options-declarations-util-mkEnableOption-magic .example}
+### `mkEnableOption` usage
 ```nix
-lib.mkEnableOption "magic"
+lib.mkEnableOption (lib.mdDoc "magic")
 # is like
 lib.mkOption {
   type = lib.types.bool;
@@ -87,8 +88,9 @@ lib.mkOption {
   description = lib.mdDoc "Whether to enable magic.";
 }
 ```
+:::
 
-### `mkPackageOption` {#sec-option-declarations-util-mkPackageOption}
+### `mkPackageOption`, `mkPackageOptionMD` {#sec-option-declarations-util-mkPackageOption}
 
 Usage:
 
@@ -100,18 +102,34 @@ Creates an Option attribute set for an option that specifies the package a modul
 
 **Note**: You shouldn’t necessarily make package options for all of your modules. You can always overwrite a specific package throughout nixpkgs by using [nixpkgs overlays](https://nixos.org/manual/nixpkgs/stable/#chap-overlays).
 
-The default package is specified as a list of strings representing its attribute path in nixpkgs. Because of this, you need to pass nixpkgs itself as the first argument.
+The package is specified in the third argument under `default` as a list of strings
+representing its attribute path in nixpkgs (or another package set).
+Because of this, you need to pass nixpkgs itself (or a subset) as the first argument.
+
+The second argument may be either a string or a list of strings.
+It provides the display name of the package in the description of the generated option
+(using only the last element if the passed value is a list)
+and serves as the fallback value for the `default` argument.
+
+To include extra information in the description, pass `extraDescription` to
+append arbitrary text to the generated description.
+You can also pass an `example` value, either a literal string or an attribute path.
 
-The second argument is the name of the option, used in the description "The \<name\> package to use.". You can also pass an example value, either a literal string or a package's attribute path.
+The default argument can be omitted if the provided name is
+an attribute of pkgs (if name is a string) or a
+valid attribute path in pkgs (if name is a list).
 
-You can omit the default path if the name of the option is also attribute path in nixpkgs.
+If you wish to explicitly provide no default, pass `null` as `default`.
 
-::: {#ex-options-declarations-util-mkPackageOption .title}
+During the transition to CommonMark documentation `mkPackageOption` creates an option with a DocBook description attribute, once the transition is completed it will create a CommonMark description instead. `mkPackageOptionMD` always creates an option with a CommonMark description attribute and will be removed some time after the transition is completed.
+
+[]{#ex-options-declarations-util-mkPackageOption}
 Examples:
 
 ::: {#ex-options-declarations-util-mkPackageOption-hello .example}
+### Simple `mkPackageOption` usage
 ```nix
-lib.mkPackageOption pkgs "hello" { }
+lib.mkPackageOptionMD pkgs "hello" { }
 # is like
 lib.mkOption {
   type = lib.types.package;
@@ -120,10 +138,12 @@ lib.mkOption {
   description = lib.mdDoc "The hello package to use.";
 }
 ```
+:::
 
 ::: {#ex-options-declarations-util-mkPackageOption-ghc .example}
+### `mkPackageOption` with explicit default and example
 ```nix
-lib.mkPackageOption pkgs "GHC" {
+lib.mkPackageOptionMD pkgs "GHC" {
   default = [ "ghc" ];
   example = "pkgs.haskell.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])";
 }
@@ -136,6 +156,23 @@ lib.mkOption {
   description = lib.mdDoc "The GHC package to use.";
 }
 ```
+:::
+
+::: {#ex-options-declarations-util-mkPackageOption-extraDescription .example}
+### `mkPackageOption` with additional description text
+```nix
+mkPackageOption pkgs [ "python39Packages" "pytorch" ] {
+  extraDescription = "This is an example and doesn't actually do anything.";
+}
+# is like
+lib.mkOption {
+  type = lib.types.package;
+  default = pkgs.python39Packages.pytorch;
+  defaultText = lib.literalExpression "pkgs.python39Packages.pytorch";
+  description = "The pytorch package to use. This is an example and doesn't actually do anything.";
+}
+```
+:::
 
 ## Extensible Option Types {#sec-option-declarations-eot}
 
@@ -149,7 +186,7 @@ multiple modules, or as an alternative to related `enable` options.
 
 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 \...).
+module file per display manager backend (sddm, gdm ...).
 
 There are two approaches we could take with this module structure:
 
@@ -184,9 +221,7 @@ changing the main service module file and the type system automatically
 enforces that there can only be a single display manager enabled.
 
 ::: {#ex-option-declaration-eot-service .example}
-::: {.title}
-**Example: Extensible type placeholder in the service module**
-:::
+### Extensible type placeholder in the service module
 ```nix
 services.xserver.displayManager.enable = mkOption {
   description = "Display manager to use";
@@ -196,9 +231,7 @@ services.xserver.displayManager.enable = mkOption {
 :::
 
 ::: {#ex-option-declaration-eot-backend-gdm .example}
-::: {.title}
-**Example: Extending `services.xserver.displayManager.enable` in the `gdm` module**
-:::
+### Extending `services.xserver.displayManager.enable` in the `gdm` module
 ```nix
 services.xserver.displayManager.enable = mkOption {
   type = with types; nullOr (enum [ "gdm" ]);
@@ -207,9 +240,7 @@ services.xserver.displayManager.enable = mkOption {
 :::
 
 ::: {#ex-option-declaration-eot-backend-sddm .example}
-::: {.title}
-**Example: Extending `services.xserver.displayManager.enable` in the `sddm` module**
-:::
+### Extending `services.xserver.displayManager.enable` in the `sddm` module
 ```nix
 services.xserver.displayManager.enable = mkOption {
   type = with types; nullOr (enum [ "sddm" ]);
diff --git a/nixos/doc/manual/development/option-def.section.md b/nixos/doc/manual/development/option-def.section.md
index 22cf38873cf..6a3dc26b99b 100644
--- a/nixos/doc/manual/development/option-def.section.md
+++ b/nixos/doc/manual/development/option-def.section.md
@@ -12,7 +12,7 @@ config = {
 However, sometimes you need to wrap an option definition or set of
 option definitions in a *property* to achieve certain effects:
 
-## Delaying Conditionals {#sec-option-definitions-delaying-conditionals .unnumbered}
+## Delaying Conditionals {#sec-option-definitions-delaying-conditionals}
 
 If a set of option definitions is conditional on the value of another
 option, you may need to use `mkIf`. Consider, for instance:
@@ -56,7 +56,7 @@ config = {
 };
 ```
 
-## Setting Priorities {#sec-option-definitions-setting-priorities .unnumbered}
+## Setting Priorities {#sec-option-definitions-setting-priorities}
 
 A module can override the definitions of an option in other modules by
 setting an *override priority*. All option definitions that do not have the lowest
@@ -72,7 +72,7 @@ This definition causes all other definitions with priorities above 10 to
 be discarded. The function `mkForce` is equal to `mkOverride 50`, and
 `mkDefault` is equal to `mkOverride 1000`.
 
-## Ordering Definitions {#sec-option-definitions-ordering .unnumbered}
+## Ordering Definitions {#sec-option-definitions-ordering}
 
 It is also possible to influence the order in which the definitions for an option are
 merged by setting an *order priority* with `mkOrder`. The default order priority is 1000.
@@ -89,7 +89,7 @@ definitions in the final list value of `hardware.firmware`.
 Note that this is different from [override priorities](#sec-option-definitions-setting-priorities):
 setting an order does not affect whether the definition is included or not.
 
-## Merging Configurations {#sec-option-definitions-merging .unnumbered}
+## Merging Configurations {#sec-option-definitions-merging}
 
 In conjunction with `mkIf`, it is sometimes useful for a module to
 return multiple sets of option definitions, to be merged together as if
diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md
index e398d6c30cc..9e2ecb8e356 100644
--- a/nixos/doc/manual/development/option-types.section.md
+++ b/nixos/doc/manual/development/option-types.section.md
@@ -36,9 +36,8 @@ merging is handled.
     together. This type is recommended when the option type is unknown.
 
     ::: {#ex-types-anything .example}
-    ::: {.title}
-    **Example: `types.anything` Example**
-    :::
+    ### `types.anything`
+
     Two definitions of this type like
 
     ```nix
@@ -92,11 +91,11 @@ merging is handled.
 :   A free-form attribute set.
 
     ::: {.warning}
-    This type will be deprecated in the future because it doesn\'t
+    This type will be deprecated in the future because it doesn't
     recurse into attribute sets, silently drops earlier attribute
-    definitions, and doesn\'t discharge `lib.mkDefault`, `lib.mkIf`
+    definitions, and doesn't discharge `lib.mkDefault`, `lib.mkIf`
     and co. For allowing arbitrary attribute sets, prefer
-    `types.attrsOf types.anything` instead which doesn\'t have these
+    `types.attrsOf types.anything` instead which doesn't have these
     problems.
     :::
 
@@ -222,7 +221,7 @@ Submodules are detailed in [Submodule](#section-option-types-submodule).
     -   *`specialArgs`* An attribute set of extra arguments to be passed
         to the module functions. The option `_module.args` should be
         used instead for most arguments since it allows overriding.
-        *`specialArgs`* should only be used for arguments that can\'t go
+        *`specialArgs`* should only be used for arguments that can't go
         through the module fixed-point, because of infinite recursion or
         other problems. An example is overriding the `lib` argument,
         because `lib` itself is used to define `_module.args`, which
@@ -236,7 +235,7 @@ Submodules are detailed in [Submodule](#section-option-types-submodule).
         In such a case it would allow the option to be set with
         `the-submodule.config = "value"` instead of requiring
         `the-submodule.config.config = "value"`. This is because
-        only when modules *don\'t* set the `config` or `options`
+        only when modules *don't* set the `config` or `options`
         keys, all keys are interpreted as option definitions in the
         `config` section. Enabling this option implicitly puts all
         attributes in the `config` section.
@@ -324,7 +323,7 @@ Composed types are types that take a type as parameter. `listOf
 :   Type *`t1`* or type *`t2`*, e.g. `with types; either int str`.
     Multiple definitions cannot be merged.
 
-`types.oneOf` \[ *`t1 t2`* \... \]
+`types.oneOf` \[ *`t1 t2`* ... \]
 
 :   Type *`t1`* or type *`t2`* and so forth, e.g.
     `with types; oneOf [ int str bool ]`. Multiple definitions cannot be
@@ -357,9 +356,7 @@ 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**
-:::
+### Directly defined submodule
 ```nix
 options.mod = mkOption {
   description = "submodule example";
@@ -378,9 +375,7 @@ options.mod = mkOption {
 :::
 
 ::: {#ex-submodule-reference .example}
-::: {.title}
-**Example: Submodule defined as a reference**
-:::
+### Submodule defined as a reference
 ```nix
 let
   modOptions = {
@@ -408,9 +403,7 @@ multiple definitions of the submodule option set
 ([Example: Definition of a list of submodules](#ex-submodule-listof-definition)).
 
 ::: {#ex-submodule-listof-declaration .example}
-::: {.title}
-**Example: Declaration of a list of submodules**
-:::
+### Declaration of a list of submodules
 ```nix
 options.mod = mkOption {
   description = "submodule example";
@@ -429,9 +422,7 @@ options.mod = mkOption {
 :::
 
 ::: {#ex-submodule-listof-definition .example}
-::: {.title}
-**Example: Definition of a list of submodules**
-:::
+### Definition of a list of submodules
 ```nix
 config.mod = [
   { foo = 1; bar = "one"; }
@@ -446,9 +437,7 @@ multiple named definitions of the submodule option set
 ([Example: Definition of attribute sets of submodules](#ex-submodule-attrsof-definition)).
 
 ::: {#ex-submodule-attrsof-declaration .example}
-::: {.title}
-**Example: Declaration of attribute sets of submodules**
-:::
+### Declaration of attribute sets of submodules
 ```nix
 options.mod = mkOption {
   description = "submodule example";
@@ -467,9 +456,7 @@ options.mod = mkOption {
 :::
 
 ::: {#ex-submodule-attrsof-definition .example}
-::: {.title}
-**Example: Definition of attribute sets of submodules**
-:::
+### Definition of attribute sets of submodules
 ```nix
 config.mod.one = { foo = 1; bar = "one"; };
 config.mod.two = { foo = 2; bar = "two"; };
@@ -489,9 +476,8 @@ Types are mainly characterized by their `check` and `merge` functions.
     ([Example: Overriding a type check](#ex-extending-type-check-2)).
 
     ::: {#ex-extending-type-check-1 .example}
-    ::: {.title}
-    **Example: Adding a type check**
-    :::
+    ### Adding a type check
+
     ```nix
     byte = mkOption {
       description = "An integer between 0 and 255.";
@@ -501,9 +487,8 @@ Types are mainly characterized by their `check` and `merge` functions.
     :::
 
     ::: {#ex-extending-type-check-2 .example}
-    ::: {.title}
-    **Example: Overriding a type check**
-    :::
+    ### Overriding a type check
+
     ```nix
     nixThings = mkOption {
       description = "words that start with 'nix'";
diff --git a/nixos/doc/manual/development/replace-modules.section.md b/nixos/doc/manual/development/replace-modules.section.md
index 0700a82004c..ac9f5adbaf9 100644
--- a/nixos/doc/manual/development/replace-modules.section.md
+++ b/nixos/doc/manual/development/replace-modules.section.md
@@ -2,19 +2,26 @@
 
 Modules that are imported can also be disabled. The option declarations,
 config implementation and the imports of a disabled module will be
-ignored, allowing another to take it\'s place. This can be used to
+ignored, allowing another to take its place. This can be used to
 import a set of modules from another channel while keeping the rest of
 the system on a stable release.
 
 `disabledModules` is a top level attribute like `imports`, `options` and
 `config`. It contains a list of modules that will be disabled. This can
-either be the full path to the module or a string with the filename
-relative to the modules path (eg. \<nixpkgs/nixos/modules> for nixos).
+either be:
+ - the full path to the module,
+ - or a string with the filename relative to the modules path (eg. \<nixpkgs/nixos/modules> for nixos),
+ - or an attribute set containing a specific `key` attribute.
+
+The latter allows some modules to be disabled, despite them being distributed
+via attributes instead of file paths. The `key` should be globally unique, so
+it is recommended to include a file path in it, or rely on a framework to do it
+for you.
 
 This example will replace the existing postgresql module with the
 version defined in the nixos-unstable channel while keeping the rest of
 the modules and packages from the original nixos channel. This only
-overrides the module definition, this won\'t use postgresql from
+overrides the module definition, this won't use postgresql from
 nixos-unstable unless explicitly configured to do so.
 
 ```nix
@@ -35,7 +42,7 @@ nixos-unstable unless explicitly configured to do so.
 
 This example shows how to define a custom module as a replacement for an
 existing module. Importing this module will disable the original module
-without having to know it\'s implementation details.
+without having to know its implementation details.
 
 ```nix
 { config, lib, pkgs, ... }:
diff --git a/nixos/doc/manual/development/running-nixos-tests-interactively.section.md b/nixos/doc/manual/development/running-nixos-tests-interactively.section.md
index 1130672cb37..54002941d63 100644
--- a/nixos/doc/manual/development/running-nixos-tests-interactively.section.md
+++ b/nixos/doc/manual/development/running-nixos-tests-interactively.section.md
@@ -24,6 +24,39 @@ back into the test driver command line upon its completion. This allows
 you to inspect the state of the VMs after the test (e.g. to debug the
 test script).
 
+## Shell access in interactive mode {#sec-nixos-test-shell-access}
+
+The function `<yourmachine>.shell_interact()` grants access to a shell running
+inside a virtual machine. To use it, replace `<yourmachine>` with the name of a
+virtual machine defined in the test, for example: `machine.shell_interact()`.
+Keep in mind that this shell may not display everything correctly as it is
+running within an interactive Python REPL, and logging output from the virtual
+machine may overwrite input and output from the guest shell:
+
+```py
+>>> machine.shell_interact()
+machine: Terminal is ready (there is no initial prompt):
+$ hostname
+machine
+```
+
+As an alternative, you can proxy the guest shell to a local TCP server by first
+starting a TCP server in a terminal using the command:
+
+```ShellSession
+$ socat 'READLINE,PROMPT=$ ' tcp-listen:4444,reuseaddr`
+```
+
+In the terminal where the test driver is running, connect to this server by
+using:
+
+```py
+>>> machine.shell_interact("tcp:127.0.0.1:4444")
+```
+
+Once the connection is established, you can enter commands in the socat terminal
+where socat is running.
+
 ## Reuse VM state {#sec-nixos-test-reuse-vm-state}
 
 You can re-use the VM states coming from a previous run by setting the
diff --git a/nixos/doc/manual/development/settings-options.section.md b/nixos/doc/manual/development/settings-options.section.md
index d569e23adbd..5060dd98f58 100644
--- a/nixos/doc/manual/development/settings-options.section.md
+++ b/nixos/doc/manual/development/settings-options.section.md
@@ -9,10 +9,10 @@ can be declared. File formats can be separated into two categories:
     `{ foo = { bar = 10; }; }`. Other examples are INI, YAML and TOML.
     The following section explains the convention for these settings.
 
--   Non-nix-representable ones: These can\'t be trivially mapped to a
+-   Non-nix-representable ones: These can't be trivially mapped to a
     subset of Nix syntax. Most generic programming languages are in this
     group, e.g. bash, since the statement `if true; then echo hi; fi`
-    doesn\'t have a trivial representation in Nix.
+    doesn't have a trivial representation in Nix.
 
     Currently there are no fixed conventions for these, but it is common
     to have a `configFile` option for setting the configuration file
@@ -24,7 +24,7 @@ can be declared. File formats can be separated into two categories:
     an `extraConfig` option of type `lines` to allow arbitrary text
     after the autogenerated part of the file.
 
-## Nix-representable Formats (JSON, YAML, TOML, INI, \...) {#sec-settings-nix-representable}
+## Nix-representable Formats (JSON, YAML, TOML, INI, ...) {#sec-settings-nix-representable}
 
 By convention, formats like this are handled with a generic `settings`
 option, representing the full program configuration as a Nix value. The
@@ -119,9 +119,8 @@ have a predefined type and string generator already declared under
         default Elixir keyword list
 
 
-::: {#pkgs-formats-result}
+[]{#pkgs-formats-result}
 These functions all return an attribute set with these values:
-:::
 
 `type`
 
@@ -144,9 +143,8 @@ These functions all return an attribute set with these values:
     :::
 
 ::: {#ex-settings-nix-representable .example}
-::: {.title}
-**Example: Module with conventional `settings` option**
-:::
+### Module with conventional `settings` option
+
 The following shows a module for an example program that uses a JSON
 configuration file. It demonstrates how above values can be used, along
 with some other related best practices. See the comments for
@@ -220,9 +218,7 @@ the port, which will enforce it to be a valid integer and make it show
 up in the manual.
 
 ::: {#ex-settings-typed-attrs .example}
-::: {.title}
-**Example: Declaring a type-checked `settings` attribute**
-:::
+### Declaring a type-checked `settings` attribute
 ```nix
 settings = lib.mkOption {
   type = lib.types.submodule {
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
index aad82831a3c..9cbec729803 100644
--- 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
@@ -47,7 +47,7 @@ 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" />
+```{=include=} sections
+unit-handling.section.md
+activation-script.section.md
 ```
diff --git a/nixos/doc/manual/development/writing-documentation.chapter.md b/nixos/doc/manual/development/writing-documentation.chapter.md
index 7c29f600d70..8d504dfb0b0 100644
--- a/nixos/doc/manual/development/writing-documentation.chapter.md
+++ b/nixos/doc/manual/development/writing-documentation.chapter.md
@@ -19,7 +19,7 @@ $ nix-shell
 nix-shell$ make
 ```
 
-Once you are done making modifications to the manual, it\'s important to
+Once you are done making modifications to the manual, it's important to
 build it before committing. You can do that as follows:
 
 ```ShellSession
@@ -83,7 +83,7 @@ Keep the following guidelines in mind when you create and add a topic:
 
 ## Adding a Topic to the Book {#sec-writing-docs-adding-a-topic}
 
-Open the parent XML file and add an `xi:include` element to the list of
+Open the parent CommonMark file and add a line to the list of
 chapters with the file name of the topic that you created. If you
 created a `section`, you add the file to the `chapter` file. If you created
 a `chapter`, you add the file to the `part` file.
diff --git a/nixos/doc/manual/development/writing-modules.chapter.md b/nixos/doc/manual/development/writing-modules.chapter.md
index 0c41cbd3cb7..e07b899e6df 100644
--- a/nixos/doc/manual/development/writing-modules.chapter.md
+++ b/nixos/doc/manual/development/writing-modules.chapter.md
@@ -37,9 +37,7 @@ options, but does not declare any. The structure of full NixOS modules
 is shown in [Example: Structure of NixOS Modules](#ex-module-syntax).
 
 ::: {#ex-module-syntax .example}
-::: {.title}
-**Example: Structure of NixOS Modules**
-:::
+### Structure of NixOS Modules
 ```nix
 { config, pkgs, ... }:
 
@@ -71,7 +69,7 @@ The meaning of each part is as follows.
 -   This `imports` list enumerates the paths to other NixOS modules that
     should be included in the evaluation of the system configuration. A
     default set of modules is defined in the file `modules/module-list.nix`.
-    These don\'t need to be added in the import list.
+    These don't need to be added in the import list.
 
 -   The attribute `options` is a nested set of *option declarations*
     (described below).
@@ -102,9 +100,7 @@ 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**
-:::
+### NixOS Module for the "locate" Service
 ```nix
 { config, lib, pkgs, ... }:
 
@@ -165,9 +161,7 @@ in {
 :::
 
 ::: {#exec-escaping-example .example}
-::: {.title}
-**Example: Escaping in Exec directives**
-:::
+### Escaping in Exec directives
 ```nix
 { config, lib, pkgs, utils, ... }:
 
@@ -195,14 +189,14 @@ in {
 ```
 :::
 
-```{=docbook}
-<xi:include href="option-declarations.section.xml" />
-<xi:include href="option-types.section.xml" />
-<xi:include href="option-def.section.xml" />
-<xi:include href="assertions.section.xml" />
-<xi:include href="meta-attributes.section.xml" />
-<xi:include href="importing-modules.section.xml" />
-<xi:include href="replace-modules.section.xml" />
-<xi:include href="freeform-modules.section.xml" />
-<xi:include href="settings-options.section.xml" />
+```{=include=} sections
+option-declarations.section.md
+option-types.section.md
+option-def.section.md
+assertions.section.md
+meta-attributes.section.md
+importing-modules.section.md
+replace-modules.section.md
+freeform-modules.section.md
+settings-options.section.md
 ```
diff --git a/nixos/doc/manual/development/writing-nixos-tests.section.md b/nixos/doc/manual/development/writing-nixos-tests.section.md
index f3edea3e704..486a4b64a26 100644
--- a/nixos/doc/manual/development/writing-nixos-tests.section.md
+++ b/nixos/doc/manual/development/writing-nixos-tests.section.md
@@ -130,6 +130,11 @@ starting them in parallel:
 start_all()
 ```
 
+If the hostname of a node contains characters that can't be used in a
+Python variable name, those characters will be replaced with
+underscores in the variable name, so `nodes.machine-a` will be exposed
+to Python as `machine_a`.
+
 ## Machine objects {#ssec-machine-objects}
 
 The following methods are available on machine objects:
@@ -165,7 +170,7 @@ The following methods are available on machine objects:
 `get_screen_text_variants`
 
 :   Return a list of different interpretations of what is currently
-    visible on the machine\'s screen using optical character
+    visible on the machine's screen using optical character
     recognition. The number and order of the interpretations is not
     specified and is subject to change, but if no exception is raised at
     least one will be returned.
@@ -177,7 +182,7 @@ The following methods are available on machine objects:
 `get_screen_text`
 
 :   Return a textual representation of what is currently visible on the
-    machine\'s screen using optical character recognition.
+    machine's screen using optical character recognition.
 
     ::: {.note}
     This requires [`enableOCR`](#test-opt-enableOCR) to be set to `true`.
@@ -273,12 +278,13 @@ The following methods are available on machine objects:
 
 `wait_for_open_port`
 
-:   Wait until a process is listening on the given TCP port (on
-    `localhost`, at least).
+:   Wait until a process is listening on the given TCP port and IP address
+    (default `localhost`).
 
 `wait_for_closed_port`
 
-:   Wait until nobody is listening on the given TCP port.
+:   Wait until nobody is listening on the given TCP port and IP address
+    (default `localhost`).
 
 `wait_for_x`
 
@@ -350,8 +356,8 @@ machine.wait_for_unit("xautolock.service", "x-session-user")
 This applies to `systemctl`, `get_unit_info`, `wait_for_unit`,
 `start_job` and `stop_job`.
 
-For faster dev cycles it\'s also possible to disable the code-linters
-(this shouldn\'t be committed though):
+For faster dev cycles it's also possible to disable the code-linters
+(this shouldn't be committed though):
 
 ```nix
 {
@@ -370,7 +376,7 @@ For faster dev cycles it\'s also possible to disable the code-linters
 
 This will produce a Nix warning at evaluation time. To fully disable the
 linter, wrap the test script in comment directives to disable the Black
-linter directly (again, don\'t commit this within the Nixpkgs
+linter directly (again, don't commit this within the Nixpkgs
 repository):
 
 ```nix
@@ -416,8 +422,7 @@ with foo_running:
 
 `seconds_interval`
 
-:
-    specifies how often the condition should be polled:
+:   specifies how often the condition should be polled:
 
 ```py
 @polling_condition(seconds_interval=10)
@@ -427,8 +432,7 @@ def foo_running():
 
 `description`
 
-:
-    is used in the log when the condition is checked. If this is not provided, the description is pulled from the docstring of the function. These two are therefore equivalent:
+:   is used in the log when the condition is checked. If this is not provided, the description is pulled from the docstring of the function. These two are therefore equivalent:
 
 ```py
 @polling_condition
@@ -471,6 +475,8 @@ In that case, `numpy` is chosen from the generic `python3Packages`.
 
 The following options can be used when writing tests.
 
-```{=docbook}
-<xi:include href="../../generated/test-options-db.xml" xpointer="test-options-list"/>
+```{=include=} options
+id-prefix: test-opt-
+list-id: test-options-list
+source: @NIXOS_TEST_OPTIONS_JSON@
 ```
diff --git a/nixos/doc/manual/from_md/README.md b/nixos/doc/manual/from_md/README.md
deleted file mode 100644
index cc6d08ca0a1..00000000000
--- a/nixos/doc/manual/from_md/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-This directory is temporarily needed while we transition the manual to CommonMark. It stores the output of the ../md-to-db.sh script that converts CommonMark files back to DocBook.
-
-We are choosing to convert the Markdown to DocBook at authoring time instead of manual building time, because we do not want the pandoc toolchain to become part of the NixOS closure.
-
-Do not edit the DocBook files inside this directory or its subdirectories. Instead, edit the corresponding .md file in the normal manual directories, and run ../md-to-db.sh to update the file here.
diff --git a/nixos/doc/manual/from_md/administration/boot-problems.section.xml b/nixos/doc/manual/from_md/administration/boot-problems.section.xml
deleted file mode 100644
index 144661c86eb..00000000000
--- a/nixos/doc/manual/from_md/administration/boot-problems.section.xml
+++ /dev/null
@@ -1,144 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-boot-problems">
-  <title>Boot Problems</title>
-  <para>
-    If NixOS fails to boot, there are a number of kernel command line
-    parameters that may help you to identify or fix the issue. You can
-    add these parameters in the GRUB boot menu by pressing “e” to modify
-    the selected boot entry and editing the line starting with
-    <literal>linux</literal>. The following are some useful kernel
-    command line parameters that are recognised by the NixOS boot
-    scripts or by systemd:
-  </para>
-  <variablelist>
-    <varlistentry>
-      <term>
-        <literal>boot.shell_on_fail</literal>
-      </term>
-      <listitem>
-        <para>
-          Allows the user to start a root shell if something goes wrong
-          in stage 1 of the boot process (the initial ramdisk). This is
-          disabled by default because there is no authentication for the
-          root shell.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>boot.debug1</literal>
-      </term>
-      <listitem>
-        <para>
-          Start an interactive shell in stage 1 before anything useful
-          has been done. That is, no modules have been loaded and no
-          file systems have been mounted, except for
-          <literal>/proc</literal> and <literal>/sys</literal>.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>boot.debug1devices</literal>
-      </term>
-      <listitem>
-        <para>
-          Like <literal>boot.debug1</literal>, but runs stage1 until
-          kernel modules are loaded and device nodes are created. This
-          may help with e.g. making the keyboard work.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>boot.debug1mounts</literal>
-      </term>
-      <listitem>
-        <para>
-          Like <literal>boot.debug1</literal> or
-          <literal>boot.debug1devices</literal>, but runs stage1 until
-          all filesystems that are mounted during initrd are mounted
-          (see
-          <link linkend="opt-fileSystems._name_.neededForBoot">neededForBoot</link>).
-          As a motivating example, this could be useful if you’ve
-          forgotten to set
-          <link linkend="opt-fileSystems._name_.neededForBoot">neededForBoot</link>
-          on a file system.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>boot.trace</literal>
-      </term>
-      <listitem>
-        <para>
-          Print every shell command executed by the stage 1 and 2 boot
-          scripts.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>single</literal>
-      </term>
-      <listitem>
-        <para>
-          Boot into rescue mode (a.k.a. single user mode). This will
-          cause systemd to start nothing but the unit
-          <literal>rescue.target</literal>, which runs
-          <literal>sulogin</literal> to prompt for the root password and
-          start a root login shell. Exiting the shell causes the system
-          to continue with the normal boot process.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>systemd.log_level=debug</literal>
-        <literal>systemd.log_target=console</literal>
-      </term>
-      <listitem>
-        <para>
-          Make systemd very verbose and send log messages to the console
-          instead of the journal. For more parameters recognised by
-          systemd, see systemd(1).
-        </para>
-      </listitem>
-    </varlistentry>
-  </variablelist>
-  <para>
-    In addition, these arguments are recognised by the live image only:
-  </para>
-  <variablelist>
-    <varlistentry>
-      <term>
-        <literal>live.nixos.passwd=password</literal>
-      </term>
-      <listitem>
-        <para>
-          Set the password for the <literal>nixos</literal> live user.
-          This can be used for SSH access if there are issues using the
-          terminal.
-        </para>
-      </listitem>
-    </varlistentry>
-  </variablelist>
-  <para>
-    Notice that for <literal>boot.shell_on_fail</literal>,
-    <literal>boot.debug1</literal>,
-    <literal>boot.debug1devices</literal>, and
-    <literal>boot.debug1mounts</literal>, if you did
-    <emphasis role="strong">not</emphasis> select <quote>start the new
-    shell as pid 1</quote>, and you <literal>exit</literal> from the new
-    shell, boot will proceed normally from the point where it failed, as
-    if you’d chosen <quote>ignore the error and continue</quote>.
-  </para>
-  <para>
-    If no login prompts or X11 login screens appear (e.g. due to hanging
-    dependencies), you can press Alt+ArrowUp. If you’re lucky, this will
-    start rescue mode (described above). (Also note that since most
-    units have a 90-second timeout before systemd gives up on them, the
-    <literal>agetty</literal> login prompts should appear eventually
-    unless something is very wrong.)
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/administration/cleaning-store.chapter.xml b/nixos/doc/manual/from_md/administration/cleaning-store.chapter.xml
deleted file mode 100644
index 4243d2bf53f..00000000000
--- a/nixos/doc/manual/from_md/administration/cleaning-store.chapter.xml
+++ /dev/null
@@ -1,72 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-nix-gc">
-  <title>Cleaning the Nix Store</title>
-  <para>
-    Nix has a purely functional model, meaning that packages are never
-    upgraded in place. Instead new versions of packages end up in a
-    different location in the Nix store (<literal>/nix/store</literal>).
-    You should periodically run Nix’s <emphasis>garbage
-    collector</emphasis> to remove old, unreferenced packages. This is
-    easy:
-  </para>
-  <programlisting>
-$ nix-collect-garbage
-</programlisting>
-  <para>
-    Alternatively, you can use a systemd unit that does the same in the
-    background:
-  </para>
-  <programlisting>
-# systemctl start nix-gc.service
-</programlisting>
-  <para>
-    You can tell NixOS in <literal>configuration.nix</literal> to run
-    this unit automatically at certain points in time, for instance,
-    every night at 03:15:
-  </para>
-  <programlisting language="bash">
-nix.gc.automatic = true;
-nix.gc.dates = &quot;03:15&quot;;
-</programlisting>
-  <para>
-    The commands above do not remove garbage collector roots, such as
-    old system configurations. Thus they do not remove the ability to
-    roll back to previous configurations. The following command deletes
-    old roots, removing the ability to roll back to them:
-  </para>
-  <programlisting>
-$ nix-collect-garbage -d
-</programlisting>
-  <para>
-    You can also do this for specific profiles, e.g.
-  </para>
-  <programlisting>
-$ nix-env -p /nix/var/nix/profiles/per-user/eelco/profile --delete-generations old
-</programlisting>
-  <para>
-    Note that NixOS system configurations are stored in the profile
-    <literal>/nix/var/nix/profiles/system</literal>.
-  </para>
-  <para>
-    Another way to reclaim disk space (often as much as 40% of the size
-    of the Nix store) is to run Nix’s store optimiser, which seeks out
-    identical files in the store and replaces them with hard links to a
-    single copy.
-  </para>
-  <programlisting>
-$ nix-store --optimise
-</programlisting>
-  <para>
-    Since this command needs to read the entire Nix store, it can take
-    quite a while to finish.
-  </para>
-  <section xml:id="sect-nixos-gc-boot-entries">
-    <title>NixOS Boot Entries</title>
-    <para>
-      If your <literal>/boot</literal> partition runs out of space,
-      after clearing old profiles you must rebuild your system with
-      <literal>nixos-rebuild boot</literal> or
-      <literal>nixos-rebuild switch</literal> to update the
-      <literal>/boot</literal> partition and clear space.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/administration/container-networking.section.xml b/nixos/doc/manual/from_md/administration/container-networking.section.xml
deleted file mode 100644
index 788a2b7b0ac..00000000000
--- a/nixos/doc/manual/from_md/administration/container-networking.section.xml
+++ /dev/null
@@ -1,54 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-container-networking">
-  <title>Container Networking</title>
-  <para>
-    When you create a container using
-    <literal>nixos-container create</literal>, it gets it own private
-    IPv4 address in the range <literal>10.233.0.0/16</literal>. You can
-    get the container’s IPv4 address as follows:
-  </para>
-  <programlisting>
-# nixos-container show-ip foo
-10.233.4.2
-
-$ ping -c1 10.233.4.2
-64 bytes from 10.233.4.2: icmp_seq=1 ttl=64 time=0.106 ms
-</programlisting>
-  <para>
-    Networking is implemented using a pair of virtual Ethernet devices.
-    The network interface in the container is called
-    <literal>eth0</literal>, while the matching interface in the host is
-    called <literal>ve-container-name</literal> (e.g.,
-    <literal>ve-foo</literal>). The container has its own network
-    namespace and the <literal>CAP_NET_ADMIN</literal> capability, so it
-    can perform arbitrary network configuration such as setting up
-    firewall rules, without affecting or having access to the host’s
-    network.
-  </para>
-  <para>
-    By default, containers cannot talk to the outside network. If you
-    want that, you should set up Network Address Translation (NAT) rules
-    on the host to rewrite container traffic to use your external IP
-    address. This can be accomplished using the following configuration
-    on the host:
-  </para>
-  <programlisting language="bash">
-networking.nat.enable = true;
-networking.nat.internalInterfaces = [&quot;ve-+&quot;];
-networking.nat.externalInterface = &quot;eth0&quot;;
-</programlisting>
-  <para>
-    where <literal>eth0</literal> should be replaced with the desired
-    external interface. Note that <literal>ve-+</literal> is a wildcard
-    that matches all container interfaces.
-  </para>
-  <para>
-    If you are using Network Manager, you need to explicitly prevent it
-    from managing container interfaces:
-  </para>
-  <programlisting language="bash">
-networking.networkmanager.unmanaged = [ &quot;interface-name:ve-*&quot; ];
-</programlisting>
-  <para>
-    You may need to restart your system for the changes to take effect.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/administration/containers.chapter.xml b/nixos/doc/manual/from_md/administration/containers.chapter.xml
deleted file mode 100644
index afbd5b35aaa..00000000000
--- a/nixos/doc/manual/from_md/administration/containers.chapter.xml
+++ /dev/null
@@ -1,31 +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" xml:id="ch-containers">
-  <title>Container Management</title>
-  <para>
-    NixOS allows you to easily run other NixOS instances as
-    <emphasis>containers</emphasis>. Containers are a light-weight
-    approach to virtualisation that runs software in the container at
-    the same speed as in the host system. NixOS containers share the Nix
-    store of the host, making container creation very efficient.
-  </para>
-  <warning>
-    <para>
-      Currently, NixOS containers are not perfectly isolated from the
-      host system. This means that a user with root access to the
-      container can do things that affect the host. So you should not
-      give container root access to untrusted users.
-    </para>
-  </warning>
-  <para>
-    NixOS containers can be created in two ways: imperatively, using the
-    command <literal>nixos-container</literal>, and declaratively, by
-    specifying them in your <literal>configuration.nix</literal>. The
-    declarative approach implies that containers get upgraded along with
-    your host system when you run <literal>nixos-rebuild</literal>,
-    which is often not what you want. By contrast, in the imperative
-    approach, containers are configured and updated independently from
-    the host system.
-  </para>
-  <xi:include href="imperative-containers.section.xml" />
-  <xi:include href="declarative-containers.section.xml" />
-  <xi:include href="container-networking.section.xml" />
-</chapter>
diff --git a/nixos/doc/manual/from_md/administration/control-groups.chapter.xml b/nixos/doc/manual/from_md/administration/control-groups.chapter.xml
deleted file mode 100644
index 8dab2c9d44b..00000000000
--- a/nixos/doc/manual/from_md/administration/control-groups.chapter.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-cgroups">
-  <title>Control Groups</title>
-  <para>
-    To keep track of the processes in a running system, systemd uses
-    <emphasis>control groups</emphasis> (cgroups). A control group is a
-    set of processes used to allocate resources such as CPU, memory or
-    I/O bandwidth. There can be multiple control group hierarchies,
-    allowing each kind of resource to be managed independently.
-  </para>
-  <para>
-    The command <literal>systemd-cgls</literal> lists all control groups
-    in the <literal>systemd</literal> hierarchy, which is what systemd
-    uses to keep track of the processes belonging to each service or
-    user session:
-  </para>
-  <programlisting>
-$ systemd-cgls
-├─user
-│ └─eelco
-│   └─c1
-│     ├─ 2567 -:0
-│     ├─ 2682 kdeinit4: kdeinit4 Running...
-│     ├─ ...
-│     └─10851 sh -c less -R
-└─system
-  ├─httpd.service
-  │ ├─2444 httpd -f /nix/store/3pyacby5cpr55a03qwbnndizpciwq161-httpd.conf -DNO_DETACH
-  │ └─...
-  ├─dhcpcd.service
-  │ └─2376 dhcpcd --config /nix/store/f8dif8dsi2yaa70n03xir8r653776ka6-dhcpcd.conf
-  └─ ...
-</programlisting>
-  <para>
-    Similarly, <literal>systemd-cgls cpu</literal> shows the cgroups in
-    the CPU hierarchy, which allows per-cgroup CPU scheduling
-    priorities. By default, every systemd service gets its own CPU
-    cgroup, while all user sessions are in the top-level CPU cgroup.
-    This ensures, for instance, that a thousand run-away processes in
-    the <literal>httpd.service</literal> cgroup cannot starve the CPU
-    for one process in the <literal>postgresql.service</literal> cgroup.
-    (By contrast, it they were in the same cgroup, then the PostgreSQL
-    process would get 1/1001 of the cgroup’s CPU time.) You can limit a
-    service’s CPU share in <literal>configuration.nix</literal>:
-  </para>
-  <programlisting language="bash">
-systemd.services.httpd.serviceConfig.CPUShares = 512;
-</programlisting>
-  <para>
-    By default, every cgroup has 1024 CPU shares, so this will halve the
-    CPU allocation of the <literal>httpd.service</literal> cgroup.
-  </para>
-  <para>
-    There also is a <literal>memory</literal> hierarchy that controls
-    memory allocation limits; by default, all processes are in the
-    top-level cgroup, so any service or session can exhaust all
-    available memory. Per-cgroup memory limits can be specified in
-    <literal>configuration.nix</literal>; for instance, to limit
-    <literal>httpd.service</literal> to 512 MiB of RAM (excluding swap):
-  </para>
-  <programlisting language="bash">
-systemd.services.httpd.serviceConfig.MemoryLimit = &quot;512M&quot;;
-</programlisting>
-  <para>
-    The command <literal>systemd-cgtop</literal> shows a continuously
-    updated list of all cgroups with their CPU and memory usage.
-  </para>
-</chapter>
diff --git a/nixos/doc/manual/from_md/administration/declarative-containers.section.xml b/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
deleted file mode 100644
index 4831c9c74e8..00000000000
--- a/nixos/doc/manual/from_md/administration/declarative-containers.section.xml
+++ /dev/null
@@ -1,60 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-declarative-containers">
-  <title>Declarative Container Specification</title>
-  <para>
-    You can also specify containers and their configuration in the
-    host’s <literal>configuration.nix</literal>. For example, the
-    following specifies that there shall be a container named
-    <literal>database</literal> running PostgreSQL:
-  </para>
-  <programlisting language="bash">
-containers.database =
-  { config =
-      { config, pkgs, ... }:
-      { services.postgresql.enable = true;
-      services.postgresql.package = pkgs.postgresql_14;
-      };
-  };
-</programlisting>
-  <para>
-    If you run <literal>nixos-rebuild switch</literal>, the container
-    will be built. If the container was already running, it will be
-    updated in place, without rebooting. The container can be configured
-    to start automatically by setting
-    <literal>containers.database.autoStart = true</literal> in its
-    configuration.
-  </para>
-  <para>
-    By default, declarative containers share the network namespace of
-    the host, meaning that they can listen on (privileged) ports.
-    However, they cannot change the network configuration. You can give
-    a container its own network as follows:
-  </para>
-  <programlisting language="bash">
-containers.database = {
-  privateNetwork = true;
-  hostAddress = &quot;192.168.100.10&quot;;
-  localAddress = &quot;192.168.100.11&quot;;
-};
-</programlisting>
-  <para>
-    This gives the container a private virtual Ethernet interface with
-    IP address <literal>192.168.100.11</literal>, which is hooked up to
-    a virtual Ethernet interface on the host with IP address
-    <literal>192.168.100.10</literal>. (See the next section for details
-    on container networking.)
-  </para>
-  <para>
-    To disable the container, just remove it from
-    <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/nixos-containers</literal>. Containers can be
-    destroyed using the imperative method:
-    <literal>nixos-container destroy foo</literal>.
-  </para>
-  <para>
-    Declarative containers can be started and stopped using the
-    corresponding systemd service, e.g.
-    <literal>systemctl start container@database</literal>.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/administration/imperative-containers.section.xml b/nixos/doc/manual/from_md/administration/imperative-containers.section.xml
deleted file mode 100644
index 865fc468939..00000000000
--- a/nixos/doc/manual/from_md/administration/imperative-containers.section.xml
+++ /dev/null
@@ -1,132 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-imperative-containers">
-  <title>Imperative Container Management</title>
-  <para>
-    We’ll cover imperative container management using
-    <literal>nixos-container</literal> first. Be aware that container
-    management is currently only possible as <literal>root</literal>.
-  </para>
-  <para>
-    You create a container with identifier <literal>foo</literal> as
-    follows:
-  </para>
-  <programlisting>
-# nixos-container create foo
-</programlisting>
-  <para>
-    This creates the container’s root directory in
-    <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
-    command line. For instance, to create a container that has
-    <literal>sshd</literal> running, with the given public key for
-    <literal>root</literal>:
-  </para>
-  <programlisting>
-# nixos-container create foo --config '
-  services.openssh.enable = true;
-  users.users.root.openssh.authorizedKeys.keys = [&quot;ssh-dss AAAAB3N…&quot;];
-'
-</programlisting>
-  <para>
-    By default the next free address in the
-    <literal>10.233.0.0/16</literal> subnet will be chosen as container
-    IP. This behavior can be altered by setting
-    <literal>--host-address</literal> and
-    <literal>--local-address</literal>:
-  </para>
-  <programlisting>
-# nixos-container create test --config-file test-container.nix \
-    --local-address 10.235.1.2 --host-address 10.235.1.1
-</programlisting>
-  <para>
-    Creating a container does not start it. To start the container, run:
-  </para>
-  <programlisting>
-# nixos-container start foo
-</programlisting>
-  <para>
-    This command will return as soon as the container has booted and has
-    reached <literal>multi-user.target</literal>. On the host, the
-    container runs within a systemd unit called
-    <literal>container@container-name.service</literal>. Thus, if
-    something went wrong, you can get status info using
-    <literal>systemctl</literal>:
-  </para>
-  <programlisting>
-# systemctl status container@foo
-</programlisting>
-  <para>
-    If the container has started successfully, you can log in as root
-    using the <literal>root-login</literal> operation:
-  </para>
-  <programlisting>
-# nixos-container root-login foo
-[root@foo:~]#
-</programlisting>
-  <para>
-    Note that only root on the host can do this (since there is no
-    authentication). You can also get a regular login prompt using the
-    <literal>login</literal> operation, which is available to all users
-    on the host:
-  </para>
-  <programlisting>
-# nixos-container login foo
-foo login: alice
-Password: ***
-</programlisting>
-  <para>
-    With <literal>nixos-container run</literal>, you can execute
-    arbitrary commands in the container:
-  </para>
-  <programlisting>
-# nixos-container run foo -- uname -a
-Linux foo 3.4.82 #1-NixOS SMP Thu Mar 20 14:44:05 UTC 2014 x86_64 GNU/Linux
-</programlisting>
-  <para>
-    There are several ways to change the configuration of the container.
-    First, on the host, you can edit
-    <literal>/var/lib/container/name/etc/nixos/configuration.nix</literal>,
-    and run
-  </para>
-  <programlisting>
-# nixos-container update foo
-</programlisting>
-  <para>
-    This will build and activate the new configuration. You can also
-    specify a new configuration on the command line:
-  </para>
-  <programlisting>
-# nixos-container update foo --config '
-  services.httpd.enable = true;
-  services.httpd.adminAddr = &quot;foo@example.org&quot;;
-  networking.firewall.allowedTCPPorts = [ 80 ];
-'
-
-# curl http://$(nixos-container show-ip foo)/
-&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 3.2 Final//EN&quot;&gt;…
-</programlisting>
-  <para>
-    However, note that this will overwrite the container’s
-    <literal>/etc/nixos/configuration.nix</literal>.
-  </para>
-  <para>
-    Alternatively, you can change the configuration from within the
-    container itself by running <literal>nixos-rebuild switch</literal>
-    inside the container. Note that the container by default does not
-    have a copy of the NixOS channel, so you should run
-    <literal>nix-channel --update</literal> first.
-  </para>
-  <para>
-    Containers can be stopped and started using
-    <literal>nixos-container stop</literal> and
-    <literal>nixos-container start</literal>, respectively, or by using
-    <literal>systemctl</literal> on the container’s service unit. To
-    destroy a container, including its file system, do
-  </para>
-  <programlisting>
-# nixos-container destroy foo
-</programlisting>
-</section>
diff --git a/nixos/doc/manual/from_md/administration/logging.chapter.xml b/nixos/doc/manual/from_md/administration/logging.chapter.xml
deleted file mode 100644
index 4da38c065a2..00000000000
--- a/nixos/doc/manual/from_md/administration/logging.chapter.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-logging">
-  <title>Logging</title>
-  <para>
-    System-wide logging is provided by systemd’s
-    <emphasis>journal</emphasis>, which subsumes traditional logging
-    daemons such as syslogd and klogd. Log entries are kept in binary
-    files in <literal>/var/log/journal/</literal>. The command
-    <literal>journalctl</literal> allows you to see the contents of the
-    journal. For example,
-  </para>
-  <programlisting>
-$ journalctl -b
-</programlisting>
-  <para>
-    shows all journal entries since the last reboot. (The output of
-    <literal>journalctl</literal> is piped into <literal>less</literal>
-    by default.) You can use various options and match operators to
-    restrict output to messages of interest. For instance, to get all
-    messages from PostgreSQL:
-  </para>
-  <programlisting>
-$ journalctl -u postgresql.service
--- Logs begin at Mon, 2013-01-07 13:28:01 CET, end at Tue, 2013-01-08 01:09:57 CET. --
-...
-Jan 07 15:44:14 hagbard postgres[2681]: [2-1] LOG:  database system is shut down
--- Reboot --
-Jan 07 15:45:10 hagbard postgres[2532]: [1-1] LOG:  database system was shut down at 2013-01-07 15:44:14 CET
-Jan 07 15:45:13 hagbard postgres[2500]: [1-1] LOG:  database system is ready to accept connections
-</programlisting>
-  <para>
-    Or to get all messages since the last reboot that have at least a
-    <quote>critical</quote> severity level:
-  </para>
-  <programlisting>
-$ journalctl -b -p crit
-Dec 17 21:08:06 mandark sudo[3673]: pam_unix(sudo:auth): auth could not identify password for [alice]
-Dec 29 01:30:22 mandark kernel[6131]: [1053513.909444] CPU6: Core temperature above threshold, cpu clock throttled (total events = 1)
-</programlisting>
-  <para>
-    The system journal is readable by root and by users in the
-    <literal>wheel</literal> and <literal>systemd-journal</literal>
-    groups. All users have a private journal that can be read using
-    <literal>journalctl</literal>.
-  </para>
-</chapter>
diff --git a/nixos/doc/manual/from_md/administration/maintenance-mode.section.xml b/nixos/doc/manual/from_md/administration/maintenance-mode.section.xml
deleted file mode 100644
index c86b1911c11..00000000000
--- a/nixos/doc/manual/from_md/administration/maintenance-mode.section.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-maintenance-mode">
-  <title>Maintenance Mode</title>
-  <para>
-    You can enter rescue mode by running:
-  </para>
-  <programlisting>
-# systemctl rescue
-</programlisting>
-  <para>
-    This will eventually give you a single-user root shell. Systemd will
-    stop (almost) all system services. To get out of maintenance mode,
-    just exit from the rescue shell.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/administration/network-problems.section.xml b/nixos/doc/manual/from_md/administration/network-problems.section.xml
deleted file mode 100644
index 4c0598ca94e..00000000000
--- a/nixos/doc/manual/from_md/administration/network-problems.section.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-nix-network-issues">
-  <title>Network Problems</title>
-  <para>
-    Nix uses a so-called <emphasis>binary cache</emphasis> to optimise
-    building a package from source into downloading it as a pre-built
-    binary. That is, whenever a command like
-    <literal>nixos-rebuild</literal> needs a path in the Nix store, Nix
-    will try to download that path from the Internet rather than build
-    it from source. The default binary cache is
-    <literal>https://cache.nixos.org/</literal>. If this cache is
-    unreachable, Nix operations may take a long time due to HTTP
-    connection timeouts. You can disable the use of the binary cache by
-    adding <literal>--option use-binary-caches false</literal>, e.g.
-  </para>
-  <programlisting>
-# nixos-rebuild switch --option use-binary-caches false
-</programlisting>
-  <para>
-    If you have an alternative binary cache at your disposal, you can
-    use it instead:
-  </para>
-  <programlisting>
-# nixos-rebuild switch --option binary-caches http://my-cache.example.org/
-</programlisting>
-</section>
diff --git a/nixos/doc/manual/from_md/administration/rebooting.chapter.xml b/nixos/doc/manual/from_md/administration/rebooting.chapter.xml
deleted file mode 100644
index 78ee75afb64..00000000000
--- a/nixos/doc/manual/from_md/administration/rebooting.chapter.xml
+++ /dev/null
@@ -1,38 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-rebooting">
-  <title>Rebooting and Shutting Down</title>
-  <para>
-    The system can be shut down (and automatically powered off) by
-    doing:
-  </para>
-  <programlisting>
-# shutdown
-</programlisting>
-  <para>
-    This is equivalent to running <literal>systemctl poweroff</literal>.
-  </para>
-  <para>
-    To reboot the system, run
-  </para>
-  <programlisting>
-# reboot
-</programlisting>
-  <para>
-    which is equivalent to <literal>systemctl reboot</literal>.
-    Alternatively, you can quickly reboot the system using
-    <literal>kexec</literal>, which bypasses the BIOS by directly
-    loading the new kernel into memory:
-  </para>
-  <programlisting>
-# systemctl kexec
-</programlisting>
-  <para>
-    The machine can be suspended to RAM (if supported) using
-    <literal>systemctl suspend</literal>, and suspended to disk using
-    <literal>systemctl hibernate</literal>.
-  </para>
-  <para>
-    These commands can be run by any user who is logged in locally, i.e.
-    on a virtual console or in X11; otherwise, the user is asked for
-    authentication.
-  </para>
-</chapter>
diff --git a/nixos/doc/manual/from_md/administration/rollback.section.xml b/nixos/doc/manual/from_md/administration/rollback.section.xml
deleted file mode 100644
index a8df053011c..00000000000
--- a/nixos/doc/manual/from_md/administration/rollback.section.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-rollback">
-  <title>Rolling Back Configuration Changes</title>
-  <para>
-    After running <literal>nixos-rebuild</literal> to switch to a new
-    configuration, you may find that the new configuration doesn’t work
-    very well. In that case, there are several ways to return to a
-    previous configuration.
-  </para>
-  <para>
-    First, the GRUB boot manager allows you to boot into any previous
-    configuration that hasn’t been garbage-collected. These
-    configurations can be found under the GRUB submenu <quote>NixOS -
-    All configurations</quote>. This is especially useful if the new
-    configuration fails to boot. After the system has booted, you can
-    make the selected configuration the default for subsequent boots:
-  </para>
-  <programlisting>
-# /run/current-system/bin/switch-to-configuration boot
-</programlisting>
-  <para>
-    Second, you can switch to the previous configuration in a running
-    system:
-  </para>
-  <programlisting>
-# nixos-rebuild switch --rollback
-</programlisting>
-  <para>
-    This is equivalent to running:
-  </para>
-  <programlisting>
-# /nix/var/nix/profiles/system-N-link/bin/switch-to-configuration switch
-</programlisting>
-  <para>
-    where <literal>N</literal> is the number of the NixOS system
-    configuration. To get a list of the available configurations, do:
-  </para>
-  <programlisting>
-$ ls -l /nix/var/nix/profiles/system-*-link
-...
-lrwxrwxrwx 1 root root 78 Aug 12 13:54 /nix/var/nix/profiles/system-268-link -&gt; /nix/store/202b...-nixos-13.07pre4932_5a676e4-4be1055
-</programlisting>
-</section>
diff --git a/nixos/doc/manual/from_md/administration/service-mgmt.chapter.xml b/nixos/doc/manual/from_md/administration/service-mgmt.chapter.xml
deleted file mode 100644
index 8b01b8f896a..00000000000
--- a/nixos/doc/manual/from_md/administration/service-mgmt.chapter.xml
+++ /dev/null
@@ -1,141 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-systemctl">
-  <title>Service Management</title>
-  <para>
-    In NixOS, all system services are started and monitored using the
-    systemd program. systemd is the <quote>init</quote> process of the
-    system (i.e. PID 1), the parent of all other processes. It manages a
-    set of so-called <quote>units</quote>, which can be things like
-    system services (programs), but also mount points, swap files,
-    devices, targets (groups of units) and more. Units can have complex
-    dependencies; for instance, one unit can require that another unit
-    must be successfully started before the first unit can be started.
-    When the system boots, it starts a unit named
-    <literal>default.target</literal>; the dependencies of this unit
-    cause all system services to be started, file systems to be mounted,
-    swap files to be activated, and so on.
-  </para>
-  <section xml:id="sect-nixos-systemd-general">
-    <title>Interacting with a running systemd</title>
-    <para>
-      The command <literal>systemctl</literal> is the main way to
-      interact with <literal>systemd</literal>. The following paragraphs
-      demonstrate ways to interact with any OS running systemd as init
-      system. NixOS is of no exception. The
-      <link linkend="sect-nixos-systemd-nixos">next section </link>
-      explains NixOS specific things worth knowing.
-    </para>
-    <para>
-      Without any arguments, <literal>systemctl</literal> the status of
-      active units:
-    </para>
-    <programlisting>
-$ systemctl
--.mount          loaded active mounted   /
-swapfile.swap    loaded active active    /swapfile
-sshd.service     loaded active running   SSH Daemon
-graphical.target loaded active active    Graphical Interface
-...
-</programlisting>
-    <para>
-      You can ask for detailed status information about a unit, for
-      instance, the PostgreSQL database service:
-    </para>
-    <programlisting>
-$ systemctl status postgresql.service
-postgresql.service - PostgreSQL Server
-          Loaded: loaded (/nix/store/pn3q73mvh75gsrl8w7fdlfk3fq5qm5mw-unit/postgresql.service)
-          Active: active (running) since Mon, 2013-01-07 15:55:57 CET; 9h ago
-        Main PID: 2390 (postgres)
-          CGroup: name=systemd:/system/postgresql.service
-                  ├─2390 postgres
-                  ├─2418 postgres: writer process
-                  ├─2419 postgres: wal writer process
-                  ├─2420 postgres: autovacuum launcher process
-                  ├─2421 postgres: stats collector process
-                  └─2498 postgres: zabbix zabbix [local] idle
-
-Jan 07 15:55:55 hagbard postgres[2394]: [1-1] LOG:  database system was shut down at 2013-01-07 15:55:05 CET
-Jan 07 15:55:57 hagbard postgres[2390]: [1-1] LOG:  database system is ready to accept connections
-Jan 07 15:55:57 hagbard postgres[2420]: [1-1] LOG:  autovacuum launcher started
-Jan 07 15:55:57 hagbard systemd[1]: Started PostgreSQL Server.
-</programlisting>
-    <para>
-      Note that this shows the status of the unit (active and running),
-      all the processes belonging to the service, as well as the most
-      recent log messages from the service.
-    </para>
-    <para>
-      Units can be stopped, started or restarted:
-    </para>
-    <programlisting>
-# systemctl stop postgresql.service
-# systemctl start postgresql.service
-# systemctl restart postgresql.service
-</programlisting>
-    <para>
-      These operations are synchronous: they wait until the service has
-      finished starting or stopping (or has failed). Starting a unit
-      will cause the dependencies of that unit to be started as well (if
-      necessary).
-    </para>
-  </section>
-  <section xml:id="sect-nixos-systemd-nixos">
-    <title>systemd in NixOS</title>
-    <para>
-      Packages in Nixpkgs sometimes provide systemd units with them,
-      usually in e.g <literal>#pkg-out#/lib/systemd/</literal>. Putting
-      such a package in <literal>environment.systemPackages</literal>
-      doesn't make the service available to users or the system.
-    </para>
-    <para>
-      In order to enable a systemd <emphasis>system</emphasis> service
-      with provided upstream package, use (e.g):
-    </para>
-    <programlisting language="bash">
-systemd.packages = [ pkgs.packagekit ];
-</programlisting>
-    <para>
-      Usually NixOS modules written by the community do the above, plus
-      take care of other details. If a module was written for a service
-      you are interested in, you'd probably need only to use
-      <literal>services.#name#.enable = true;</literal>. These services
-      are defined in Nixpkgs'
-      <link xlink:href="https://github.com/NixOS/nixpkgs/tree/master/nixos/modules">
-      <literal>nixos/modules/</literal> directory </link>. In case the
-      service is simple enough, the above method should work, and start
-      the service on boot.
-    </para>
-    <para>
-      <emphasis>User</emphasis> systemd services on the other hand,
-      should be treated differently. Given a package that has a systemd
-      unit file at <literal>#pkg-out#/lib/systemd/user/</literal>, using
-      <xref linkend="opt-systemd.packages" /> will make you able to
-      start the service via <literal>systemctl --user start</literal>,
-      but it won't start automatically on login. However, You can
-      imperatively enable it by adding the package's attribute to
-      <xref linkend="opt-systemd.packages" /> and then do this (e.g):
-    </para>
-    <programlisting>
-$ mkdir -p ~/.config/systemd/user/default.target.wants
-$ ln -s /run/current-system/sw/lib/systemd/user/syncthing.service ~/.config/systemd/user/default.target.wants/
-$ systemctl --user daemon-reload
-$ systemctl --user enable syncthing.service
-</programlisting>
-    <para>
-      If you are interested in a timer file, use
-      <literal>timers.target.wants</literal> instead of
-      <literal>default.target.wants</literal> in the 1st and 2nd
-      command.
-    </para>
-    <para>
-      Using <literal>systemctl --user enable syncthing.service</literal>
-      instead of the above, will work, but it'll use the absolute path
-      of <literal>syncthing.service</literal> for the symlink, and this
-      path is in <literal>/nix/store/.../lib/systemd/user/</literal>.
-      Hence <link linkend="sec-nix-gc">garbage collection</link> will
-      remove that file and you will wind up with a broken symlink in
-      your systemd configuration, which in turn will not make the
-      service / timer start on login.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/administration/store-corruption.section.xml b/nixos/doc/manual/from_md/administration/store-corruption.section.xml
deleted file mode 100644
index 9ed572d484d..00000000000
--- a/nixos/doc/manual/from_md/administration/store-corruption.section.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-nix-store-corruption">
-  <title>Nix Store Corruption</title>
-  <para>
-    After a system crash, it’s possible for files in the Nix store to
-    become corrupted. (For instance, the Ext4 file system has the
-    tendency to replace un-synced files with zero bytes.) NixOS tries
-    hard to prevent this from happening: it performs a
-    <literal>sync</literal> before switching to a new configuration, and
-    Nix’s database is fully transactional. If corruption still occurs,
-    you may be able to fix it automatically.
-  </para>
-  <para>
-    If the corruption is in a path in the closure of the NixOS system
-    configuration, you can fix it by doing
-  </para>
-  <programlisting>
-# nixos-rebuild switch --repair
-</programlisting>
-  <para>
-    This will cause Nix to check every path in the closure, and if its
-    cryptographic hash differs from the hash recorded in Nix’s database,
-    the path is rebuilt or redownloaded.
-  </para>
-  <para>
-    You can also scan the entire Nix store for corrupt paths:
-  </para>
-  <programlisting>
-# nix-store --verify --check-contents --repair
-</programlisting>
-  <para>
-    Any corrupt paths will be redownloaded if they’re available in a
-    binary cache; otherwise, they cannot be repaired.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/administration/troubleshooting.chapter.xml b/nixos/doc/manual/from_md/administration/troubleshooting.chapter.xml
deleted file mode 100644
index 8bbb8a1fe72..00000000000
--- a/nixos/doc/manual/from_md/administration/troubleshooting.chapter.xml
+++ /dev/null
@@ -1,12 +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" xml:id="ch-troubleshooting">
-  <title>Troubleshooting</title>
-  <para>
-    This chapter describes solutions to common problems you might
-    encounter when you manage your NixOS system.
-  </para>
-  <xi:include href="boot-problems.section.xml" />
-  <xi:include href="maintenance-mode.section.xml" />
-  <xi:include href="rollback.section.xml" />
-  <xi:include href="store-corruption.section.xml" />
-  <xi:include href="network-problems.section.xml" />
-</chapter>
diff --git a/nixos/doc/manual/from_md/administration/user-sessions.chapter.xml b/nixos/doc/manual/from_md/administration/user-sessions.chapter.xml
deleted file mode 100644
index e8c64f153fc..00000000000
--- a/nixos/doc/manual/from_md/administration/user-sessions.chapter.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-user-sessions">
-  <title>User Sessions</title>
-  <para>
-    Systemd keeps track of all users who are logged into the system
-    (e.g. on a virtual console or remotely via SSH). The command
-    <literal>loginctl</literal> allows querying and manipulating user
-    sessions. For instance, to list all user sessions:
-  </para>
-  <programlisting>
-$ loginctl
-   SESSION        UID USER             SEAT
-        c1        500 eelco            seat0
-        c3          0 root             seat0
-        c4        500 alice
-</programlisting>
-  <para>
-    This shows that two users are logged in locally, while another is
-    logged in remotely. (<quote>Seats</quote> are essentially the
-    combinations of displays and input devices attached to the system;
-    usually, there is only one seat.) To get information about a
-    session:
-  </para>
-  <programlisting>
-$ loginctl session-status c3
-c3 - root (0)
-           Since: Tue, 2013-01-08 01:17:56 CET; 4min 42s ago
-          Leader: 2536 (login)
-            Seat: seat0; vc3
-             TTY: /dev/tty3
-         Service: login; type tty; class user
-           State: online
-          CGroup: name=systemd:/user/root/c3
-                  ├─ 2536 /nix/store/10mn4xip9n7y9bxqwnsx7xwx2v2g34xn-shadow-4.1.5.1/bin/login --
-                  ├─10339 -bash
-                  └─10355 w3m nixos.org
-</programlisting>
-  <para>
-    This shows that the user is logged in on virtual console 3. It also
-    lists the processes belonging to this session. Since systemd keeps
-    track of this, you can terminate a session in a way that ensures
-    that all the session’s processes are gone:
-  </para>
-  <programlisting>
-# loginctl terminate-session c3
-</programlisting>
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/abstractions.section.xml b/nixos/doc/manual/from_md/configuration/abstractions.section.xml
deleted file mode 100644
index c71e23e34ad..00000000000
--- a/nixos/doc/manual/from_md/configuration/abstractions.section.xml
+++ /dev/null
@@ -1,101 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-module-abstractions">
-  <title>Abstractions</title>
-  <para>
-    If you find yourself repeating yourself over and over, it’s time to
-    abstract. Take, for instance, this Apache HTTP Server configuration:
-  </para>
-  <programlisting language="bash">
-{
-  services.httpd.virtualHosts =
-    { &quot;blog.example.org&quot; = {
-        documentRoot = &quot;/webroot/blog.example.org&quot;;
-        adminAddr = &quot;alice@example.org&quot;;
-        forceSSL = true;
-        enableACME = true;
-        enablePHP = true;
-      };
-      &quot;wiki.example.org&quot; = {
-        documentRoot = &quot;/webroot/wiki.example.org&quot;;
-        adminAddr = &quot;alice@example.org&quot;;
-        forceSSL = true;
-        enableACME = true;
-        enablePHP = true;
-      };
-    };
-}
-</programlisting>
-  <para>
-    It defines two virtual hosts with nearly identical configuration;
-    the only difference is the document root directories. To prevent
-    this duplication, we can use a <literal>let</literal>:
-  </para>
-  <programlisting language="bash">
-let
-  commonConfig =
-    { adminAddr = &quot;alice@example.org&quot;;
-      forceSSL = true;
-      enableACME = true;
-    };
-in
-{
-  services.httpd.virtualHosts =
-    { &quot;blog.example.org&quot; = (commonConfig // { documentRoot = &quot;/webroot/blog.example.org&quot;; });
-      &quot;wiki.example.org&quot; = (commonConfig // { documentRoot = &quot;/webroot/wiki.example.com&quot;; });
-    };
-}
-</programlisting>
-  <para>
-    The <literal>let commonConfig = ...</literal> defines a variable
-    named <literal>commonConfig</literal>. The <literal>//</literal>
-    operator merges two attribute sets, so the configuration of the
-    second virtual host is the set <literal>commonConfig</literal>
-    extended with the document root option.
-  </para>
-  <para>
-    You can write a <literal>let</literal> wherever an expression is
-    allowed. Thus, you also could have written:
-  </para>
-  <programlisting language="bash">
-{
-  services.httpd.virtualHosts =
-    let commonConfig = ...; in
-    { &quot;blog.example.org&quot; = (commonConfig // { ... })
-      &quot;wiki.example.org&quot; = (commonConfig // { ... })
-    };
-}
-</programlisting>
-  <para>
-    but not <literal>{ let commonConfig = ...; in ...; }</literal> since
-    attributes (as opposed to attribute values) are not expressions.
-  </para>
-  <para>
-    <emphasis role="strong">Functions</emphasis> provide another method
-    of abstraction. For instance, suppose that we want to generate lots
-    of different virtual hosts, all with identical configuration except
-    for the document root. This can be done as follows:
-  </para>
-  <programlisting language="bash">
-{
-  services.httpd.virtualHosts =
-    let
-      makeVirtualHost = webroot:
-        { documentRoot = webroot;
-          adminAddr = &quot;alice@example.org&quot;;
-          forceSSL = true;
-          enableACME = true;
-        };
-    in
-      { &quot;example.org&quot; = (makeVirtualHost &quot;/webroot/example.org&quot;);
-        &quot;example.com&quot; = (makeVirtualHost &quot;/webroot/example.com&quot;);
-        &quot;example.gov&quot; = (makeVirtualHost &quot;/webroot/example.gov&quot;);
-        &quot;example.nl&quot; = (makeVirtualHost &quot;/webroot/example.nl&quot;);
-      };
-}
-</programlisting>
-  <para>
-    Here, <literal>makeVirtualHost</literal> is a function that takes a
-    single argument <literal>webroot</literal> and returns the
-    configuration for a virtual host. That function is then called for
-    several names to produce the list of virtual host configurations.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/ad-hoc-network-config.section.xml b/nixos/doc/manual/from_md/configuration/ad-hoc-network-config.section.xml
deleted file mode 100644
index 035ee3122e1..00000000000
--- a/nixos/doc/manual/from_md/configuration/ad-hoc-network-config.section.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="ad-hoc-network-config">
-  <title>Ad-Hoc Configuration</title>
-  <para>
-    You can use <xref linkend="opt-networking.localCommands" /> to
-    specify shell commands to be run at the end of
-    <literal>network-setup.service</literal>. This is useful for doing
-    network configuration not covered by the existing NixOS modules. For
-    instance, to statically configure an IPv6 address:
-  </para>
-  <programlisting language="bash">
-networking.localCommands =
-  ''
-    ip -6 addr add 2001:610:685:1::1/64 dev eth0
-  '';
-</programlisting>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/ad-hoc-packages.section.xml b/nixos/doc/manual/from_md/configuration/ad-hoc-packages.section.xml
deleted file mode 100644
index c9a8d4f3f10..00000000000
--- a/nixos/doc/manual/from_md/configuration/ad-hoc-packages.section.xml
+++ /dev/null
@@ -1,59 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-ad-hoc-packages">
-  <title>Ad-Hoc Package Management</title>
-  <para>
-    With the command <literal>nix-env</literal>, you can install and
-    uninstall packages from the command line. For instance, to install
-    Mozilla Thunderbird:
-  </para>
-  <programlisting>
-$ nix-env -iA nixos.thunderbird
-</programlisting>
-  <para>
-    If you invoke this as root, the package is installed in the Nix
-    profile <literal>/nix/var/nix/profiles/default</literal> and visible
-    to all users of the system; otherwise, the package ends up in
-    <literal>/nix/var/nix/profiles/per-user/username/profile</literal>
-    and is not visible to other users. The <literal>-A</literal> flag
-    specifies the package by its attribute name; without it, the package
-    is installed by matching against its package name (e.g.
-    <literal>thunderbird</literal>). The latter is slower because it
-    requires matching against all available Nix packages, and is
-    ambiguous if there are multiple matching packages.
-  </para>
-  <para>
-    Packages come from the NixOS channel. You typically upgrade a
-    package by updating to the latest version of the NixOS channel:
-  </para>
-  <programlisting>
-$ nix-channel --update nixos
-</programlisting>
-  <para>
-    and then running <literal>nix-env -i</literal> again. Other packages
-    in the profile are <emphasis>not</emphasis> affected; this is the
-    crucial difference with the declarative style of package management,
-    where running <literal>nixos-rebuild switch</literal> causes all
-    packages to be updated to their current versions in the NixOS
-    channel. You can however upgrade all packages for which there is a
-    newer version by doing:
-  </para>
-  <programlisting>
-$ nix-env -u '*'
-</programlisting>
-  <para>
-    A package can be uninstalled using the <literal>-e</literal> flag:
-  </para>
-  <programlisting>
-$ nix-env -e thunderbird
-</programlisting>
-  <para>
-    Finally, you can roll back an undesirable <literal>nix-env</literal>
-    action:
-  </para>
-  <programlisting>
-$ nix-env --rollback
-</programlisting>
-  <para>
-    <literal>nix-env</literal> has many more flags. For details, see the
-    nix-env(1) manpage or the Nix manual.
-  </para>
-</section>
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
deleted file mode 100644
index 07f541666cb..00000000000
--- a/nixos/doc/manual/from_md/configuration/adding-custom-packages.section.xml
+++ /dev/null
@@ -1,118 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-custom-packages">
-  <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. 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>
-  <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">
-environment.systemPackages = [ pkgs.my-package ];
-</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">
-environment.systemPackages =
-  let
-    my-hello = with pkgs; stdenv.mkDerivation rec {
-      name = &quot;hello-2.8&quot;;
-      src = fetchurl {
-        url = &quot;mirror://gnu/hello/${name}.tar.gz&quot;;
-        sha256 = &quot;0wqd8sjmxfskrflaxywc7gqw7sfawrfvdxd9skxawzfgyy0pzdz6&quot;;
-      };
-    };
-  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">
-environment.systemPackages = [ (import ./my-hello.nix) ];
-</programlisting>
-    <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 {
-  name = &quot;hello-2.8&quot;;
-  src = fetchurl {
-    url = &quot;mirror://gnu/hello/${name}.tar.gz&quot;;
-    sha256 = &quot;0wqd8sjmxfskrflaxywc7gqw7sfawrfvdxd9skxawzfgyy0pzdz6&quot;;
-  };
-}
-</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/config-file.section.xml b/nixos/doc/manual/from_md/configuration/config-file.section.xml
deleted file mode 100644
index 9792116eb08..00000000000
--- a/nixos/doc/manual/from_md/configuration/config-file.section.xml
+++ /dev/null
@@ -1,231 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-configuration-file">
-  <title>NixOS Configuration File</title>
-  <para>
-    The NixOS configuration file generally looks like this:
-  </para>
-  <programlisting language="bash">
-{ config, pkgs, ... }:
-
-{ option definitions
-}
-</programlisting>
-  <para>
-    The first line (<literal>{ config, pkgs, ... }:</literal>) denotes
-    that this is actually a function that takes at least the two
-    arguments <literal>config</literal> and <literal>pkgs</literal>.
-    (These are explained later, in chapter
-    <xref linkend="sec-writing-modules" />) The function returns a
-    <emphasis>set</emphasis> of option definitions
-    (<literal>{ ... }</literal>). These definitions have the form
-    <literal>name = value</literal>, where <literal>name</literal> is
-    the name of an option and <literal>value</literal> is its value. For
-    example,
-  </para>
-  <programlisting language="bash">
-{ config, pkgs, ... }:
-
-{ services.httpd.enable = true;
-  services.httpd.adminAddr = &quot;alice@example.org&quot;;
-  services.httpd.virtualHosts.localhost.documentRoot = &quot;/webroot&quot;;
-}
-</programlisting>
-  <para>
-    defines a configuration with three option definitions that together
-    enable the Apache HTTP Server with <literal>/webroot</literal> as
-    the document root.
-  </para>
-  <para>
-    Sets can be nested, and in fact dots in option names are shorthand
-    for defining a set containing another set. For instance,
-    <xref linkend="opt-services.httpd.enable" /> defines a set named
-    <literal>services</literal> that contains a set named
-    <literal>httpd</literal>, which in turn contains an option
-    definition named <literal>enable</literal> with value
-    <literal>true</literal>. This means that the example above can also
-    be written as:
-  </para>
-  <programlisting language="bash">
-{ config, pkgs, ... }:
-
-{ services = {
-    httpd = {
-      enable = true;
-      adminAddr = &quot;alice@example.org&quot;;
-      virtualHosts = {
-        localhost = {
-          documentRoot = &quot;/webroot&quot;;
-        };
-      };
-    };
-  };
-}
-</programlisting>
-  <para>
-    which may be more convenient if you have lots of option definitions
-    that share the same prefix (such as
-    <literal>services.httpd</literal>).
-  </para>
-  <para>
-    NixOS checks your option definitions for correctness. For instance,
-    if you try to define an option that doesn’t exist (that is, doesn’t
-    have a corresponding <emphasis>option declaration</emphasis>),
-    <literal>nixos-rebuild</literal> will give an error like:
-  </para>
-  <programlisting>
-The option `services.httpd.enable' defined in `/etc/nixos/configuration.nix' does not exist.
-</programlisting>
-  <para>
-    Likewise, values in option definitions must have a correct type. For
-    instance, <literal>services.httpd.enable</literal> must be a Boolean
-    (<literal>true</literal> or <literal>false</literal>). Trying to
-    give it a value of another type, such as a string, will cause an
-    error:
-  </para>
-  <programlisting>
-The option value `services.httpd.enable' in `/etc/nixos/configuration.nix' is not a boolean.
-</programlisting>
-  <para>
-    Options have various types of values. The most important are:
-  </para>
-  <variablelist>
-    <varlistentry>
-      <term>
-        Strings
-      </term>
-      <listitem>
-        <para>
-          Strings are enclosed in double quotes, e.g.
-        </para>
-        <programlisting language="bash">
-networking.hostName = &quot;dexter&quot;;
-</programlisting>
-        <para>
-          Special characters can be escaped by prefixing them with a
-          backslash (e.g. <literal>\&quot;</literal>).
-        </para>
-        <para>
-          Multi-line strings can be enclosed in <emphasis>double single
-          quotes</emphasis>, e.g.
-        </para>
-        <programlisting language="bash">
-networking.extraHosts =
-  ''
-    127.0.0.2 other-localhost
-    10.0.0.1 server
-  '';
-</programlisting>
-        <para>
-          The main difference is that it strips from each line a number
-          of spaces equal to the minimal indentation of the string as a
-          whole (disregarding the indentation of empty lines), and that
-          characters like <literal>&quot;</literal> and
-          <literal>\</literal> are not special (making it more
-          convenient for including things like shell code). See more
-          info about this in the Nix manual
-          <link xlink:href="https://nixos.org/nix/manual/#ssec-values">here</link>.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        Booleans
-      </term>
-      <listitem>
-        <para>
-          These can be <literal>true</literal> or
-          <literal>false</literal>, e.g.
-        </para>
-        <programlisting language="bash">
-networking.firewall.enable = true;
-networking.firewall.allowPing = false;
-</programlisting>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        Integers
-      </term>
-      <listitem>
-        <para>
-          For example,
-        </para>
-        <programlisting language="bash">
-boot.kernel.sysctl.&quot;net.ipv4.tcp_keepalive_time&quot; = 60;
-</programlisting>
-        <para>
-          (Note that here the attribute name
-          <literal>net.ipv4.tcp_keepalive_time</literal> is enclosed in
-          quotes to prevent it from being interpreted as a set named
-          <literal>net</literal> containing a set named
-          <literal>ipv4</literal>, and so on. This is because it’s not a
-          NixOS option but the literal name of a Linux kernel setting.)
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        Sets
-      </term>
-      <listitem>
-        <para>
-          Sets were introduced above. They are name/value pairs enclosed
-          in braces, as in the option definition
-        </para>
-        <programlisting language="bash">
-fileSystems.&quot;/boot&quot; =
-  { device = &quot;/dev/sda1&quot;;
-    fsType = &quot;ext4&quot;;
-    options = [ &quot;rw&quot; &quot;data=ordered&quot; &quot;relatime&quot; ];
-  };
-</programlisting>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        Lists
-      </term>
-      <listitem>
-        <para>
-          The important thing to note about lists is that list elements
-          are separated by whitespace, like this:
-        </para>
-        <programlisting language="bash">
-boot.kernelModules = [ &quot;fuse&quot; &quot;kvm-intel&quot; &quot;coretemp&quot; ];
-</programlisting>
-        <para>
-          List elements can be any other type, e.g. sets:
-        </para>
-        <programlisting language="bash">
-swapDevices = [ { device = &quot;/dev/disk/by-label/swap&quot;; } ];
-</programlisting>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        Packages
-      </term>
-      <listitem>
-        <para>
-          Usually, the packages you need are already part of the Nix
-          Packages collection, which is a set that can be accessed
-          through the function argument <literal>pkgs</literal>. Typical
-          uses:
-        </para>
-        <programlisting language="bash">
-environment.systemPackages =
-  [ pkgs.thunderbird
-    pkgs.emacs
-  ];
-
-services.postgresql.package = pkgs.postgresql_14;
-</programlisting>
-        <para>
-          The latter option definition changes the default PostgreSQL
-          package used by NixOS’s PostgreSQL service to 10.x. For more
-          information on packages, including how to add new ones, see
-          <xref linkend="sec-custom-packages" />.
-        </para>
-      </listitem>
-    </varlistentry>
-  </variablelist>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/config-syntax.chapter.xml b/nixos/doc/manual/from_md/configuration/config-syntax.chapter.xml
deleted file mode 100644
index baf9639554c..00000000000
--- a/nixos/doc/manual/from_md/configuration/config-syntax.chapter.xml
+++ /dev/null
@@ -1,20 +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" xml:id="sec-configuration-syntax">
-  <title>Configuration Syntax</title>
-  <para>
-    The NixOS configuration file
-    <literal>/etc/nixos/configuration.nix</literal> is actually a
-    <emphasis>Nix expression</emphasis>, which is the Nix package
-    manager’s purely functional language for describing how to build
-    packages and configurations. This means you have all the expressive
-    power of that language at your disposal, including the ability to
-    abstract over common patterns, which is very useful when managing
-    complex systems. The syntax and semantics of the Nix language are
-    fully described in the
-    <link xlink:href="https://nixos.org/nix/manual/#chap-writing-nix-expressions">Nix
-    manual</link>, but here we give a short overview of the most
-    important constructs useful in NixOS configuration files.
-  </para>
-  <xi:include href="config-file.section.xml" />
-  <xi:include href="abstractions.section.xml" />
-  <xi:include href="modularity.section.xml" />
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/customizing-packages.section.xml b/nixos/doc/manual/from_md/configuration/customizing-packages.section.xml
deleted file mode 100644
index f78b5dc5460..00000000000
--- a/nixos/doc/manual/from_md/configuration/customizing-packages.section.xml
+++ /dev/null
@@ -1,90 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-customising-packages">
-  <title>Customising Packages</title>
-  <para>
-    Some packages in Nixpkgs have options to enable or disable optional
-    functionality or change other aspects of the package. For instance,
-    the Firefox wrapper package (which provides Firefox with a set of
-    plugins such as the Adobe Flash player) has an option to enable the
-    Google Talk plugin. It can be set in
-    <literal>configuration.nix</literal> as follows:
-    <literal>nixpkgs.config.firefox.enableGoogleTalkPlugin = true;</literal>
-  </para>
-  <warning>
-    <para>
-      Unfortunately, Nixpkgs currently lacks a way to query available
-      configuration options.
-    </para>
-  </warning>
-  <para>
-    Apart from high-level options, it’s possible to tweak a package in
-    almost arbitrary ways, such as changing or disabling dependencies of
-    a package. For instance, the Emacs package in Nixpkgs by default has
-    a dependency on GTK 2. If you want to build it against GTK 3, you
-    can specify that as follows:
-  </para>
-  <programlisting language="bash">
-environment.systemPackages = [ (pkgs.emacs.override { gtk = pkgs.gtk3; }) ];
-</programlisting>
-  <para>
-    The function <literal>override</literal> performs the call to the
-    Nix function that produces Emacs, with the original arguments
-    amended by the set of arguments specified by you. So here the
-    function argument <literal>gtk</literal> gets the value
-    <literal>pkgs.gtk3</literal>, causing Emacs to depend on GTK 3. (The
-    parentheses are necessary because in Nix, function application binds
-    more weakly than list construction, so without them,
-    <xref linkend="opt-environment.systemPackages" /> would be a list
-    with two elements.)
-  </para>
-  <para>
-    Even greater customisation is possible using the function
-    <literal>overrideAttrs</literal>. While the
-    <literal>override</literal> mechanism above overrides the arguments
-    of a package function, <literal>overrideAttrs</literal> allows
-    changing the <emphasis>attributes</emphasis> passed to
-    <literal>mkDerivation</literal>. This permits changing any aspect of
-    the package, such as the source code. For instance, if you want to
-    override the source code of Emacs, you can say:
-  </para>
-  <programlisting language="bash">
-environment.systemPackages = [
-  (pkgs.emacs.overrideAttrs (oldAttrs: {
-    name = &quot;emacs-25.0-pre&quot;;
-    src = /path/to/my/emacs/tree;
-  }))
-];
-</programlisting>
-  <para>
-    Here, <literal>overrideAttrs</literal> takes the Nix derivation
-    specified by <literal>pkgs.emacs</literal> and produces a new
-    derivation in which the original’s <literal>name</literal> and
-    <literal>src</literal> attribute have been replaced by the given
-    values by re-calling <literal>stdenv.mkDerivation</literal>. The
-    original attributes are accessible via the function argument, which
-    is conventionally named <literal>oldAttrs</literal>.
-  </para>
-  <para>
-    The overrides shown above are not global. They do not affect the
-    original package; other packages in Nixpkgs continue to depend on
-    the original rather than the customised package. This means that if
-    another package in your system depends on the original package, you
-    end up with two instances of the package. If you want to have
-    everything depend on your customised instance, you can apply a
-    <emphasis>global</emphasis> override as follows:
-  </para>
-  <programlisting language="bash">
-nixpkgs.config.packageOverrides = pkgs:
-  { emacs = pkgs.emacs.override { gtk = pkgs.gtk3; };
-  };
-</programlisting>
-  <para>
-    The effect of this definition is essentially equivalent to modifying
-    the <literal>emacs</literal> attribute in the Nixpkgs source tree.
-    Any package in Nixpkgs that depends on <literal>emacs</literal> will
-    be passed your customised instance. (However, the value
-    <literal>pkgs.emacs</literal> in
-    <literal>nixpkgs.config.packageOverrides</literal> refers to the
-    original rather than overridden instance, to prevent an infinite
-    recursion.)
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/declarative-packages.section.xml b/nixos/doc/manual/from_md/configuration/declarative-packages.section.xml
deleted file mode 100644
index da31f18d923..00000000000
--- a/nixos/doc/manual/from_md/configuration/declarative-packages.section.xml
+++ /dev/null
@@ -1,53 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook"  xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" xml:id="sec-declarative-package-mgmt">
-  <title>Declarative Package Management</title>
-  <para>
-    With declarative package management, you specify which packages you
-    want on your system by setting the option
-    <xref linkend="opt-environment.systemPackages" />. For instance,
-    adding the following line to <literal>configuration.nix</literal>
-    enables the Mozilla Thunderbird email application:
-  </para>
-  <programlisting language="bash">
-environment.systemPackages = [ pkgs.thunderbird ];
-</programlisting>
-  <para>
-    The effect of this specification is that the Thunderbird package
-    from Nixpkgs will be built or downloaded as part of the system when
-    you run <literal>nixos-rebuild switch</literal>.
-  </para>
-  <note>
-    <para>
-      Some packages require additional global configuration such as
-      D-Bus or systemd service registration so adding them to
-      <xref linkend="opt-environment.systemPackages" /> might not be
-      sufficient. You are advised to check the
-      <link linkend="ch-options">list of options</link> whether a NixOS
-      module for the package does not exist.
-    </para>
-  </note>
-  <para>
-    You can get a list of the available packages as follows:
-  </para>
-  <programlisting>
-$ nix-env -qaP '*' --description
-nixos.firefox   firefox-23.0   Mozilla Firefox - the browser, reloaded
-...
-</programlisting>
-  <para>
-    The first column in the output is the <emphasis>attribute
-    name</emphasis>, such as <literal>nixos.thunderbird</literal>.
-  </para>
-  <para>
-    Note: the <literal>nixos</literal> prefix tells us that we want to
-    get the package from the <literal>nixos</literal> channel and works
-    only in CLI tools. In declarative configuration use
-    <literal>pkgs</literal> prefix (variable).
-  </para>
-  <para>
-    To <quote>uninstall</quote> a package, simply remove it from
-    <xref linkend="opt-environment.systemPackages" /> and run
-    <literal>nixos-rebuild switch</literal>.
-  </para>
-  <xi:include href="customizing-packages.section.xml" />
-  <xi:include href="adding-custom-packages.section.xml" />
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/file-systems.chapter.xml b/nixos/doc/manual/from_md/configuration/file-systems.chapter.xml
deleted file mode 100644
index 71441d8b4a5..00000000000
--- a/nixos/doc/manual/from_md/configuration/file-systems.chapter.xml
+++ /dev/null
@@ -1,55 +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" xml:id="ch-file-systems">
-  <title>File Systems</title>
-  <para>
-    You can define file systems using the <literal>fileSystems</literal>
-    configuration option. For instance, the following definition causes
-    NixOS to mount the Ext4 file system on device
-    <literal>/dev/disk/by-label/data</literal> onto the mount point
-    <literal>/data</literal>:
-  </para>
-  <programlisting language="bash">
-fileSystems.&quot;/data&quot; =
-  { device = &quot;/dev/disk/by-label/data&quot;;
-    fsType = &quot;ext4&quot;;
-  };
-</programlisting>
-  <para>
-    This will create an entry in <literal>/etc/fstab</literal>, which
-    will generate a corresponding
-    <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.mount.html">systemd.mount</link>
-    unit via
-    <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd-fstab-generator.html">systemd-fstab-generator</link>.
-    The filesystem will be mounted automatically unless
-    <literal>&quot;noauto&quot;</literal> is present in
-    <link linkend="opt-fileSystems._name_.options">options</link>.
-    <literal>&quot;noauto&quot;</literal> filesystems can be mounted
-    explicitly using <literal>systemctl</literal> e.g.
-    <literal>systemctl start data.mount</literal>. Mount points are
-    created automatically if they don’t already exist. For
-    <literal>device</literal>, it’s best to use the topology-independent
-    device aliases in <literal>/dev/disk/by-label</literal> and
-    <literal>/dev/disk/by-uuid</literal>, as these don’t change if the
-    topology changes (e.g. if a disk is moved to another IDE
-    controller).
-  </para>
-  <para>
-    You can usually omit the file system type
-    (<literal>fsType</literal>), since <literal>mount</literal> can
-    usually detect the type and load the necessary kernel module
-    automatically. However, if the file system is needed at early boot
-    (in the initial ramdisk) and is not <literal>ext2</literal>,
-    <literal>ext3</literal> or <literal>ext4</literal>, then it’s best
-    to specify <literal>fsType</literal> to ensure that the kernel
-    module is available.
-  </para>
-  <note>
-    <para>
-      System startup will fail if any of the filesystems fails to mount,
-      dropping you to the emergency shell. You can make a mount
-      asynchronous and non-critical by adding
-      <literal>options = [ &quot;nofail&quot; ];</literal>.
-    </para>
-  </note>
-  <xi:include href="luks-file-systems.section.xml" />
-  <xi:include href="sshfs-file-systems.section.xml" />
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/firewall.section.xml b/nixos/doc/manual/from_md/configuration/firewall.section.xml
deleted file mode 100644
index 24c19bb1c66..00000000000
--- a/nixos/doc/manual/from_md/configuration/firewall.section.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-firewall">
-  <title>Firewall</title>
-  <para>
-    NixOS has a simple stateful firewall that blocks incoming
-    connections and other unexpected packets. The firewall applies to
-    both IPv4 and IPv6 traffic. It is enabled by default. It can be
-    disabled as follows:
-  </para>
-  <programlisting language="bash">
-networking.firewall.enable = false;
-</programlisting>
-  <para>
-    If the firewall is enabled, you can open specific TCP ports to the
-    outside world:
-  </para>
-  <programlisting language="bash">
-networking.firewall.allowedTCPPorts = [ 80 443 ];
-</programlisting>
-  <para>
-    Note that TCP port 22 (ssh) is opened automatically if the SSH
-    daemon is enabled
-    (<literal>services.openssh.enable = true</literal>). UDP ports can
-    be opened through
-    <xref linkend="opt-networking.firewall.allowedUDPPorts" />.
-  </para>
-  <para>
-    To open ranges of TCP ports:
-  </para>
-  <programlisting language="bash">
-networking.firewall.allowedTCPPortRanges = [
-  { from = 4000; to = 4007; }
-  { from = 8000; to = 8010; }
-];
-</programlisting>
-  <para>
-    Similarly, UDP port ranges can be opened through
-    <xref linkend="opt-networking.firewall.allowedUDPPortRanges" />.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/gpu-accel.chapter.xml b/nixos/doc/manual/from_md/configuration/gpu-accel.chapter.xml
deleted file mode 100644
index 90d2c17e12e..00000000000
--- a/nixos/doc/manual/from_md/configuration/gpu-accel.chapter.xml
+++ /dev/null
@@ -1,281 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-gpu-accel">
-  <title>GPU acceleration</title>
-  <para>
-    NixOS provides various APIs that benefit from GPU hardware
-    acceleration, such as VA-API and VDPAU for video playback; OpenGL
-    and Vulkan for 3D graphics; and OpenCL for general-purpose
-    computing. This chapter describes how to set up GPU hardware
-    acceleration (as far as this is not done automatically) and how to
-    verify that hardware acceleration is indeed used.
-  </para>
-  <para>
-    Most of the aforementioned APIs are agnostic with regards to which
-    display server is used. Consequently, these instructions should
-    apply both to the X Window System and Wayland compositors.
-  </para>
-  <section xml:id="sec-gpu-accel-opencl">
-    <title>OpenCL</title>
-    <para>
-      <link xlink:href="https://en.wikipedia.org/wiki/OpenCL">OpenCL</link>
-      is a general compute API. It is used by various applications such
-      as Blender and Darktable to accelerate certain operations.
-    </para>
-    <para>
-      OpenCL applications load drivers through the <emphasis>Installable
-      Client Driver</emphasis> (ICD) mechanism. In this mechanism, an
-      ICD file specifies the path to the OpenCL driver for a particular
-      GPU family. In NixOS, there are two ways to make ICD files visible
-      to the ICD loader. The first is through the
-      <literal>OCL_ICD_VENDORS</literal> environment variable. This
-      variable can contain a directory which is scanned by the ICL
-      loader for ICD files. For example:
-    </para>
-    <programlisting>
-$ export \
-  OCL_ICD_VENDORS=`nix-build '&lt;nixpkgs&gt;' --no-out-link -A rocm-opencl-icd`/etc/OpenCL/vendors/
-</programlisting>
-    <para>
-      The second mechanism is to add the OpenCL driver package to
-      <xref linkend="opt-hardware.opengl.extraPackages" />. This links
-      the ICD file under <literal>/run/opengl-driver</literal>, where it
-      will be visible to the ICD loader.
-    </para>
-    <para>
-      The proper installation of OpenCL drivers can be verified through
-      the <literal>clinfo</literal> command of the clinfo package. This
-      command will report the number of hardware devices that is found
-      and give detailed information for each device:
-    </para>
-    <programlisting>
-$ clinfo | head -n3
-Number of platforms  1
-Platform Name        AMD Accelerated Parallel Processing
-Platform Vendor      Advanced Micro Devices, Inc.
-</programlisting>
-    <section xml:id="sec-gpu-accel-opencl-amd">
-      <title>AMD</title>
-      <para>
-        Modern AMD
-        <link xlink:href="https://en.wikipedia.org/wiki/Graphics_Core_Next">Graphics
-        Core Next</link> (GCN) GPUs are supported through the
-        rocm-opencl-icd package. Adding this package to
-        <xref linkend="opt-hardware.opengl.extraPackages" /> enables
-        OpenCL support:
-      </para>
-      <programlisting language="bash">
-hardware.opengl.extraPackages = [
-  rocm-opencl-icd
-];
-</programlisting>
-    </section>
-    <section xml:id="sec-gpu-accel-opencl-intel">
-      <title>Intel</title>
-      <para>
-        <link xlink:href="https://en.wikipedia.org/wiki/List_of_Intel_graphics_processing_units#Gen8">Intel
-        Gen8 and later GPUs</link> are supported by the Intel NEO OpenCL
-        runtime that is provided by the intel-compute-runtime package.
-        For Gen7 GPUs, the deprecated Beignet runtime can be used, which
-        is provided by the beignet package. The proprietary Intel OpenCL
-        runtime, in the intel-ocl package, is an alternative for Gen7
-        GPUs.
-      </para>
-      <para>
-        The intel-compute-runtime, beignet, or intel-ocl package can be
-        added to <xref linkend="opt-hardware.opengl.extraPackages" /> to
-        enable OpenCL support. For example, for Gen8 and later GPUs, the
-        following configuration can be used:
-      </para>
-      <programlisting language="bash">
-hardware.opengl.extraPackages = [
-  intel-compute-runtime
-];
-</programlisting>
-    </section>
-  </section>
-  <section xml:id="sec-gpu-accel-vulkan">
-    <title>Vulkan</title>
-    <para>
-      <link xlink:href="https://en.wikipedia.org/wiki/Vulkan_(API)">Vulkan</link>
-      is a graphics and compute API for GPUs. It is used directly by
-      games or indirectly though compatibility layers like
-      <link xlink:href="https://github.com/doitsujin/dxvk/wiki">DXVK</link>.
-    </para>
-    <para>
-      By default, if <xref linkend="opt-hardware.opengl.driSupport" />
-      is enabled, mesa is installed and provides Vulkan for supported
-      hardware.
-    </para>
-    <para>
-      Similar to OpenCL, Vulkan drivers are loaded through the
-      <emphasis>Installable Client Driver</emphasis> (ICD) mechanism.
-      ICD files for Vulkan are JSON files that specify the path to the
-      driver library and the supported Vulkan version. All successfully
-      loaded drivers are exposed to the application as different GPUs.
-      In NixOS, there are two ways to make ICD files visible to Vulkan
-      applications: an environment variable and a module option.
-    </para>
-    <para>
-      The first option is through the
-      <literal>VK_ICD_FILENAMES</literal> environment variable. This
-      variable can contain multiple JSON files, separated by
-      <literal>:</literal>. For example:
-    </para>
-    <programlisting>
-$ export \
-  VK_ICD_FILENAMES=`nix-build '&lt;nixpkgs&gt;' --no-out-link -A amdvlk`/share/vulkan/icd.d/amd_icd64.json
-</programlisting>
-    <para>
-      The second mechanism is to add the Vulkan driver package to
-      <xref linkend="opt-hardware.opengl.extraPackages" />. This links
-      the ICD file under <literal>/run/opengl-driver</literal>, where it
-      will be visible to the ICD loader.
-    </para>
-    <para>
-      The proper installation of Vulkan drivers can be verified through
-      the <literal>vulkaninfo</literal> command of the vulkan-tools
-      package. This command will report the hardware devices and drivers
-      found, in this example output amdvlk and radv:
-    </para>
-    <programlisting>
-$ vulkaninfo | grep GPU
-                GPU id  : 0 (Unknown AMD GPU)
-                GPU id  : 1 (AMD RADV NAVI10 (LLVM 9.0.1))
-     ...
-GPU0:
-        deviceType     = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
-        deviceName     = Unknown AMD GPU
-GPU1:
-        deviceType     = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU
-</programlisting>
-    <para>
-      A simple graphical application that uses Vulkan is
-      <literal>vkcube</literal> from the vulkan-tools package.
-    </para>
-    <section xml:id="sec-gpu-accel-vulkan-amd">
-      <title>AMD</title>
-      <para>
-        Modern AMD
-        <link xlink:href="https://en.wikipedia.org/wiki/Graphics_Core_Next">Graphics
-        Core Next</link> (GCN) GPUs are supported through either radv,
-        which is part of mesa, or the amdvlk package. Adding the amdvlk
-        package to <xref linkend="opt-hardware.opengl.extraPackages" />
-        makes amdvlk the default driver and hides radv and lavapipe from
-        the device list. A specific driver can be forced as follows:
-      </para>
-      <programlisting language="bash">
-hardware.opengl.extraPackages = [
-  pkgs.amdvlk
-];
-
-# To enable Vulkan support for 32-bit applications, also add:
-hardware.opengl.extraPackages32 = [
-  pkgs.driversi686Linux.amdvlk
-];
-
-# Force radv
-environment.variables.AMD_VULKAN_ICD = &quot;RADV&quot;;
-# Or
-environment.variables.VK_ICD_FILENAMES =
-  &quot;/run/opengl-driver/share/vulkan/icd.d/radeon_icd.x86_64.json&quot;;
-</programlisting>
-    </section>
-  </section>
-  <section xml:id="sec-gpu-accel-va-api">
-    <title>VA-API</title>
-    <para>
-      <link xlink:href="https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html">VA-API
-      (Video Acceleration API)</link> is an open-source library and API
-      specification, which provides access to graphics hardware
-      acceleration capabilities for video processing.
-    </para>
-    <para>
-      VA-API drivers are loaded by <literal>libva</literal>. The version
-      in nixpkgs is built to search the opengl driver path, so drivers
-      can be installed in
-      <xref linkend="opt-hardware.opengl.extraPackages" />.
-    </para>
-    <para>
-      VA-API can be tested using:
-    </para>
-    <programlisting>
-$ nix-shell -p libva-utils --run vainfo
-</programlisting>
-    <section xml:id="sec-gpu-accel-va-api-intel">
-      <title>Intel</title>
-      <para>
-        Modern Intel GPUs use the iHD driver, which can be installed
-        with:
-      </para>
-      <programlisting language="bash">
-hardware.opengl.extraPackages = [
-  intel-media-driver
-];
-</programlisting>
-      <para>
-        Older Intel GPUs use the i965 driver, which can be installed
-        with:
-      </para>
-      <programlisting language="bash">
-hardware.opengl.extraPackages = [
-  vaapiIntel
-];
-</programlisting>
-    </section>
-  </section>
-  <section xml:id="sec-gpu-accel-common-issues">
-    <title>Common issues</title>
-    <section xml:id="sec-gpu-accel-common-issues-permissions">
-      <title>User permissions</title>
-      <para>
-        Except where noted explicitly, it should not be necessary to
-        adjust user permissions to use these acceleration APIs. In the
-        default configuration, GPU devices have world-read/write
-        permissions (<literal>/dev/dri/renderD*</literal>) or are tagged
-        as <literal>uaccess</literal>
-        (<literal>/dev/dri/card*</literal>). The access control lists of
-        devices with the <literal>uaccess</literal> tag will be updated
-        automatically when a user logs in through
-        <literal>systemd-logind</literal>. For example, if the user
-        <emphasis>alice</emphasis> is logged in, the access control list
-        should look as follows:
-      </para>
-      <programlisting>
-$ getfacl /dev/dri/card0
-# file: dev/dri/card0
-# owner: root
-# group: video
-user::rw-
-user:alice:rw-
-group::rw-
-mask::rw-
-other::---
-</programlisting>
-      <para>
-        If you disabled (this functionality of)
-        <literal>systemd-logind</literal>, you may need to add the user
-        to the <literal>video</literal> group and log in again.
-      </para>
-    </section>
-    <section xml:id="sec-gpu-accel-common-issues-mixing-nixpkgs">
-      <title>Mixing different versions of nixpkgs</title>
-      <para>
-        The <emphasis>Installable Client Driver</emphasis> (ICD)
-        mechanism used by OpenCL and Vulkan loads runtimes into its
-        address space using <literal>dlopen</literal>. Mixing an ICD
-        loader mechanism and runtimes from different version of nixpkgs
-        may not work. For example, if the ICD loader uses an older
-        version of glibc than the runtime, the runtime may not be
-        loadable due to missing symbols. Unfortunately, the loader will
-        generally be quiet about such issues.
-      </para>
-      <para>
-        If you suspect that you are running into library version
-        mismatches between an ICL loader and a runtime, you could run an
-        application with the <literal>LD_DEBUG</literal> variable set to
-        get more diagnostic information. For example, OpenCL can be
-        tested with <literal>LD_DEBUG=files clinfo</literal>, which
-        should report missing symbols.
-      </para>
-    </section>
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/ipv4-config.section.xml b/nixos/doc/manual/from_md/configuration/ipv4-config.section.xml
deleted file mode 100644
index 047ba2165f0..00000000000
--- a/nixos/doc/manual/from_md/configuration/ipv4-config.section.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-ipv4">
-  <title>IPv4 Configuration</title>
-  <para>
-    By default, NixOS uses DHCP (specifically,
-    <literal>dhcpcd</literal>) to automatically configure network
-    interfaces. However, you can configure an interface manually as
-    follows:
-  </para>
-  <programlisting language="bash">
-networking.interfaces.eth0.ipv4.addresses = [ {
-  address = &quot;192.168.1.2&quot;;
-  prefixLength = 24;
-} ];
-</programlisting>
-  <para>
-    Typically you’ll also want to set a default gateway and set of name
-    servers:
-  </para>
-  <programlisting language="bash">
-networking.defaultGateway = &quot;192.168.1.1&quot;;
-networking.nameservers = [ &quot;8.8.8.8&quot; ];
-</programlisting>
-  <note>
-    <para>
-      Statically configured interfaces are set up by the systemd service
-      <literal>interface-name-cfg.service</literal>. The default gateway
-      and name server configuration is performed by
-      <literal>network-setup.service</literal>.
-    </para>
-  </note>
-  <para>
-    The host name is set using
-    <xref linkend="opt-networking.hostName" />:
-  </para>
-  <programlisting language="bash">
-networking.hostName = &quot;cartman&quot;;
-</programlisting>
-  <para>
-    The default host name is <literal>nixos</literal>. Set it to the
-    empty string (<literal>&quot;&quot;</literal>) to allow the DHCP
-    server to provide the host name.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/ipv6-config.section.xml b/nixos/doc/manual/from_md/configuration/ipv6-config.section.xml
deleted file mode 100644
index 137c3d772a8..00000000000
--- a/nixos/doc/manual/from_md/configuration/ipv6-config.section.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-ipv6">
-  <title>IPv6 Configuration</title>
-  <para>
-    IPv6 is enabled by default. Stateless address autoconfiguration is
-    used to automatically assign IPv6 addresses to all interfaces, and
-    Privacy Extensions (RFC 4946) are enabled by default. You can adjust
-    the default for this by setting
-    <xref linkend="opt-networking.tempAddresses" />. This option may be
-    overridden on a per-interface basis by
-    <xref linkend="opt-networking.interfaces._name_.tempAddress" />. You
-    can disable IPv6 support globally by setting:
-  </para>
-  <programlisting language="bash">
-networking.enableIPv6 = false;
-</programlisting>
-  <para>
-    You can disable IPv6 on a single interface using a normal sysctl (in
-    this example, we use interface <literal>eth0</literal>):
-  </para>
-  <programlisting language="bash">
-boot.kernel.sysctl.&quot;net.ipv6.conf.eth0.disable_ipv6&quot; = true;
-</programlisting>
-  <para>
-    As with IPv4 networking interfaces are automatically configured via
-    DHCPv6. You can configure an interface manually:
-  </para>
-  <programlisting language="bash">
-networking.interfaces.eth0.ipv6.addresses = [ {
-  address = &quot;fe00:aa:bb:cc::2&quot;;
-  prefixLength = 64;
-} ];
-</programlisting>
-  <para>
-    For configuring a gateway, optionally with explicitly specified
-    interface:
-  </para>
-  <programlisting language="bash">
-networking.defaultGateway6 = {
-  address = &quot;fe00::1&quot;;
-  interface = &quot;enp0s3&quot;;
-};
-</programlisting>
-  <para>
-    See <xref linkend="sec-ipv4" /> for similar examples and additional
-    information.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/kubernetes.chapter.xml b/nixos/doc/manual/from_md/configuration/kubernetes.chapter.xml
deleted file mode 100644
index 1de19f64bda..00000000000
--- a/nixos/doc/manual/from_md/configuration/kubernetes.chapter.xml
+++ /dev/null
@@ -1,115 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-kubernetes">
-  <title>Kubernetes</title>
-  <para>
-    The NixOS Kubernetes module is a collective term for a handful of
-    individual submodules implementing the Kubernetes cluster
-    components.
-  </para>
-  <para>
-    There are generally two ways of enabling Kubernetes on NixOS. One
-    way is to enable and configure cluster components appropriately by
-    hand:
-  </para>
-  <programlisting language="bash">
-services.kubernetes = {
-  apiserver.enable = true;
-  controllerManager.enable = true;
-  scheduler.enable = true;
-  addonManager.enable = true;
-  proxy.enable = true;
-  flannel.enable = true;
-};
-</programlisting>
-  <para>
-    Another way is to assign cluster roles (&quot;master&quot; and/or
-    &quot;node&quot;) to the host. This enables apiserver,
-    controllerManager, scheduler, addonManager, kube-proxy and etcd:
-  </para>
-  <programlisting language="bash">
-services.kubernetes.roles = [ &quot;master&quot; ];
-</programlisting>
-  <para>
-    While this will enable the kubelet and kube-proxy only:
-  </para>
-  <programlisting language="bash">
-services.kubernetes.roles = [ &quot;node&quot; ];
-</programlisting>
-  <para>
-    Assigning both the master and node roles is usable if you want a
-    single node Kubernetes cluster for dev or testing purposes:
-  </para>
-  <programlisting language="bash">
-services.kubernetes.roles = [ &quot;master&quot; &quot;node&quot; ];
-</programlisting>
-  <para>
-    Note: Assigning either role will also default both
-    <xref linkend="opt-services.kubernetes.flannel.enable" /> and
-    <xref linkend="opt-services.kubernetes.easyCerts" /> to true. This
-    sets up flannel as CNI and activates automatic PKI bootstrapping.
-  </para>
-  <note>
-    <para>
-      As of NixOS 19.03, it is mandatory to configure:
-      <xref linkend="opt-services.kubernetes.masterAddress" />. The
-      masterAddress must be resolveable and routeable by all cluster
-      nodes. In single node clusters, this can be set to
-      <literal>localhost</literal>.
-    </para>
-  </note>
-  <para>
-    Role-based access control (RBAC) authorization mode is enabled by
-    default. This means that anonymous requests to the apiserver secure
-    port will expectedly cause a permission denied error. All cluster
-    components must therefore be configured with x509 certificates for
-    two-way tls communication. The x509 certificate subject section
-    determines the roles and permissions granted by the apiserver to
-    perform clusterwide or namespaced operations. See also:
-    <link xlink:href="https://kubernetes.io/docs/reference/access-authn-authz/rbac/">
-    Using RBAC Authorization</link>.
-  </para>
-  <para>
-    The NixOS kubernetes module provides an option for automatic
-    certificate bootstrapping and configuration,
-    <xref linkend="opt-services.kubernetes.easyCerts" />. The PKI
-    bootstrapping process involves setting up a certificate authority
-    (CA) daemon (cfssl) on the kubernetes master node. cfssl generates a
-    CA-cert for the cluster, and uses the CA-cert for signing
-    subordinate certs issued to each of the cluster components.
-    Subsequently, the certmgr daemon monitors active certificates and
-    renews them when needed. For single node Kubernetes clusters,
-    setting <xref linkend="opt-services.kubernetes.easyCerts" /> = true
-    is sufficient and no further action is required. For joining extra
-    node machines to an existing cluster on the other hand, establishing
-    initial trust is mandatory.
-  </para>
-  <para>
-    To add new nodes to the cluster: On any (non-master) cluster node
-    where <xref linkend="opt-services.kubernetes.easyCerts" /> is
-    enabled, the helper script
-    <literal>nixos-kubernetes-node-join</literal> is available on PATH.
-    Given a token on stdin, it will copy the token to the kubernetes
-    secrets directory and restart the certmgr service. As requested
-    certificates are issued, the script will restart kubernetes cluster
-    components as needed for them to pick up new keypairs.
-  </para>
-  <note>
-    <para>
-      Multi-master (HA) clusters are not supported by the easyCerts
-      module.
-    </para>
-  </note>
-  <para>
-    In order to interact with an RBAC-enabled cluster as an
-    administrator, one needs to have cluster-admin privileges. By
-    default, when easyCerts is enabled, a cluster-admin kubeconfig file
-    is generated and linked into
-    <literal>/etc/kubernetes/cluster-admin.kubeconfig</literal> as
-    determined by
-    <xref linkend="opt-services.kubernetes.pki.etcClusterAdminKubeconfig" />.
-    <literal>export KUBECONFIG=/etc/kubernetes/cluster-admin.kubeconfig</literal>
-    will make kubectl use this kubeconfig to access and authenticate the
-    cluster. The cluster-admin kubeconfig references an auto-generated
-    keypair owned by root. Thus, only root on the kubernetes master may
-    obtain cluster-admin rights by means of this file.
-  </para>
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/linux-kernel.chapter.xml b/nixos/doc/manual/from_md/configuration/linux-kernel.chapter.xml
deleted file mode 100644
index dd570e1d66c..00000000000
--- a/nixos/doc/manual/from_md/configuration/linux-kernel.chapter.xml
+++ /dev/null
@@ -1,204 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-kernel-config">
-  <title>Linux Kernel</title>
-  <para>
-    You can override the Linux kernel and associated packages using the
-    option <literal>boot.kernelPackages</literal>. For instance, this
-    selects the Linux 3.10 kernel:
-  </para>
-  <programlisting language="bash">
-boot.kernelPackages = pkgs.linuxKernel.packages.linux_3_10;
-</programlisting>
-  <para>
-    Note that this not only replaces the kernel, but also packages that
-    are specific to the kernel version, such as the NVIDIA video
-    drivers. This ensures that driver packages are consistent with the
-    kernel.
-  </para>
-  <para>
-    While <literal>pkgs.linuxKernel.packages</literal> contains all
-    available kernel packages, you may want to use one of the
-    unversioned <literal>pkgs.linuxPackages_*</literal> aliases such as
-    <literal>pkgs.linuxPackages_latest</literal>, that are kept up to
-    date with new versions.
-  </para>
-  <para>
-    Please note that the current convention in NixOS is to only keep
-    actively maintained kernel versions on both unstable and the
-    currently supported stable release(s) of NixOS. This means that a
-    non-longterm kernel will be removed after it’s abandoned by the
-    kernel developers, even on stable NixOS versions. If you pin your
-    kernel onto a non-longterm version, expect your evaluation to fail
-    as soon as the version is out of maintenance.
-  </para>
-  <para>
-    Longterm versions of kernels will be removed before the next stable
-    NixOS that will exceed the maintenance period of the kernel version.
-  </para>
-  <para>
-    The default Linux kernel configuration should be fine for most
-    users. You can see the configuration of your current kernel with the
-    following command:
-  </para>
-  <programlisting>
-zcat /proc/config.gz
-</programlisting>
-  <para>
-    If you want to change the kernel configuration, you can use the
-    <literal>packageOverrides</literal> feature (see
-    <xref linkend="sec-customising-packages" />). For instance, to
-    enable support for the kernel debugger KGDB:
-  </para>
-  <programlisting language="bash">
-nixpkgs.config.packageOverrides = pkgs: pkgs.lib.recursiveUpdate pkgs {
-  linuxKernel.kernels.linux_5_10 = pkgs.linuxKernel.kernels.linux_5_10.override {
-    extraConfig = ''
-      KGDB y
-    '';
-  };
-};
-</programlisting>
-  <para>
-    <literal>extraConfig</literal> takes a list of Linux kernel
-    configuration options, one per line. The name of the option should
-    not include the prefix <literal>CONFIG_</literal>. The option value
-    is typically <literal>y</literal>, <literal>n</literal> or
-    <literal>m</literal> (to build something as a kernel module).
-  </para>
-  <para>
-    Kernel modules for hardware devices are generally loaded
-    automatically by <literal>udev</literal>. You can force a module to
-    be loaded via <xref linkend="opt-boot.kernelModules" />, e.g.
-  </para>
-  <programlisting language="bash">
-boot.kernelModules = [ &quot;fuse&quot; &quot;kvm-intel&quot; &quot;coretemp&quot; ];
-</programlisting>
-  <para>
-    If the module is required early during the boot (e.g. to mount the
-    root file system), you can use
-    <xref linkend="opt-boot.initrd.kernelModules" />:
-  </para>
-  <programlisting language="bash">
-boot.initrd.kernelModules = [ &quot;cifs&quot; ];
-</programlisting>
-  <para>
-    This causes the specified modules and their dependencies to be added
-    to the initial ramdisk.
-  </para>
-  <para>
-    Kernel runtime parameters can be set through
-    <xref linkend="opt-boot.kernel.sysctl" />, e.g.
-  </para>
-  <programlisting language="bash">
-boot.kernel.sysctl.&quot;net.ipv4.tcp_keepalive_time&quot; = 120;
-</programlisting>
-  <para>
-    sets the kernel’s TCP keepalive time to 120 seconds. To see the
-    available parameters, run <literal>sysctl -a</literal>.
-  </para>
-  <section xml:id="sec-linux-config-customizing">
-    <title>Customize your kernel</title>
-    <para>
-      The first step before compiling the kernel is to generate an
-      appropriate <literal>.config</literal> configuration. Either you
-      pass your own config via the <literal>configfile</literal> setting
-      of <literal>linuxKernel.manualConfig</literal>:
-    </para>
-    <programlisting language="bash">
-custom-kernel = let base_kernel = linuxKernel.kernels.linux_4_9;
-  in super.linuxKernel.manualConfig {
-    inherit (super) stdenv hostPlatform;
-    inherit (base_kernel) src;
-    version = &quot;${base_kernel.version}-custom&quot;;
-
-    configfile = /home/me/my_kernel_config;
-    allowImportFromDerivation = true;
-};
-</programlisting>
-    <para>
-      You can edit the config with this snippet (by default
-      <literal>make menuconfig</literal> won't work out of the box on
-      nixos):
-    </para>
-    <programlisting>
-nix-shell -E 'with import &lt;nixpkgs&gt; {}; kernelToOverride.overrideAttrs (o: {nativeBuildInputs=o.nativeBuildInputs ++ [ pkg-config ncurses ];})'
-</programlisting>
-    <para>
-      or you can let nixpkgs generate the configuration. Nixpkgs
-      generates it via answering the interactive kernel utility
-      <literal>make config</literal>. The answers depend on parameters
-      passed to
-      <literal>pkgs/os-specific/linux/kernel/generic.nix</literal>
-      (which you can influence by overriding
-      <literal>extraConfig, autoModules, modDirVersion, preferBuiltin, extraConfig</literal>).
-    </para>
-    <programlisting language="bash">
-mptcp93.override ({
-  name=&quot;mptcp-local&quot;;
-
-  ignoreConfigErrors = true;
-  autoModules = false;
-  kernelPreferBuiltin = true;
-
-  enableParallelBuilding = true;
-
-  extraConfig = ''
-    DEBUG_KERNEL y
-    FRAME_POINTER y
-    KGDB y
-    KGDB_SERIAL_CONSOLE y
-    DEBUG_INFO y
-  '';
-});
-</programlisting>
-  </section>
-  <section xml:id="sec-linux-config-developing-modules">
-    <title>Developing kernel modules</title>
-    <para>
-      When developing kernel modules it's often convenient to run
-      edit-compile-run loop as quickly as possible. See below snippet as
-      an example of developing <literal>mellanox</literal> drivers.
-    </para>
-    <programlisting>
-$ nix-build '&lt;nixpkgs&gt;' -A linuxPackages.kernel.dev
-$ nix-shell '&lt;nixpkgs&gt;' -A linuxPackages.kernel
-$ unpackPhase
-$ cd linux-*
-$ make -C $dev/lib/modules/*/build M=$(pwd)/drivers/net/ethernet/mellanox modules
-# insmod ./drivers/net/ethernet/mellanox/mlx5/core/mlx5_core.ko
-</programlisting>
-  </section>
-  <section xml:id="sec-linux-zfs">
-    <title>ZFS</title>
-    <para>
-      It’s a common issue that the latest stable version of ZFS doesn’t
-      support the latest available Linux kernel. It is recommended to
-      use the latest available LTS that’s compatible with ZFS. Usually
-      this is the default kernel provided by nixpkgs (i.e.
-      <literal>pkgs.linuxPackages</literal>).
-    </para>
-    <para>
-      Alternatively, it’s possible to pin the system to the latest
-      available kernel version <emphasis>that is supported by
-      ZFS</emphasis> like this:
-    </para>
-    <programlisting language="bash">
-{
-  boot.kernelPackages = pkgs.zfs.latestCompatibleLinuxPackages;
-}
-</programlisting>
-    <para>
-      Please note that the version this attribute points to isn’t
-      monotonic because the latest kernel version only refers to kernel
-      versions supported by the Linux developers. In other words, the
-      latest kernel version that ZFS is compatible with may decrease
-      over time.
-    </para>
-    <para>
-      An example: the latest version ZFS is compatible with is 5.19
-      which is a non-longterm version. When 5.19 is out of maintenance,
-      the latest supported kernel version is 5.15 because it’s longterm
-      and the versions 5.16, 5.17 and 5.18 are already out of
-      maintenance because they’re non-longterm.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/luks-file-systems.section.xml b/nixos/doc/manual/from_md/configuration/luks-file-systems.section.xml
deleted file mode 100644
index 42b766eba98..00000000000
--- a/nixos/doc/manual/from_md/configuration/luks-file-systems.section.xml
+++ /dev/null
@@ -1,84 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-luks-file-systems">
-  <title>LUKS-Encrypted File Systems</title>
-  <para>
-    NixOS supports file systems that are encrypted using
-    <emphasis>LUKS</emphasis> (Linux Unified Key Setup). For example,
-    here is how you create an encrypted Ext4 file system on the device
-    <literal>/dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d</literal>:
-  </para>
-  <programlisting>
-# cryptsetup luksFormat /dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d
-
-WARNING!
-========
-This will overwrite data on /dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d irrevocably.
-
-Are you sure? (Type uppercase yes): YES
-Enter LUKS passphrase: ***
-Verify passphrase: ***
-
-# cryptsetup luksOpen /dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d crypted
-Enter passphrase for /dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d: ***
-
-# mkfs.ext4 /dev/mapper/crypted
-</programlisting>
-  <para>
-    The LUKS volume should be automatically picked up by
-    <literal>nixos-generate-config</literal>, but you might want to
-    verify that your <literal>hardware-configuration.nix</literal> looks
-    correct. To manually ensure that the system is automatically mounted
-    at boot time as <literal>/</literal>, add the following to
-    <literal>configuration.nix</literal>:
-  </para>
-  <programlisting language="bash">
-boot.initrd.luks.devices.crypted.device = &quot;/dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d&quot;;
-fileSystems.&quot;/&quot;.device = &quot;/dev/mapper/crypted&quot;;
-</programlisting>
-  <para>
-    Should grub be used as bootloader, and <literal>/boot</literal> is
-    located on an encrypted partition, it is necessary to add the
-    following grub option:
-  </para>
-  <programlisting language="bash">
-boot.loader.grub.enableCryptodisk = true;
-</programlisting>
-  <section xml:id="sec-luks-file-systems-fido2">
-    <title>FIDO2</title>
-    <para>
-      NixOS also supports unlocking your LUKS-Encrypted file system
-      using a FIDO2 compatible token. In the following example, we will
-      create a new FIDO2 credential and add it as a new key to our
-      existing device <literal>/dev/sda2</literal>:
-    </para>
-    <programlisting>
-# export FIDO2_LABEL=&quot;/dev/sda2 @ $HOSTNAME&quot;
-# fido2luks credential &quot;$FIDO2_LABEL&quot;
-f1d00200108b9d6e849a8b388da457688e3dd653b4e53770012d8f28e5d3b269865038c346802f36f3da7278b13ad6a3bb6a1452e24ebeeaa24ba40eef559b1b287d2a2f80b7
-
-# fido2luks -i add-key /dev/sda2 f1d00200108b9d6e849a8b388da457688e3dd653b4e53770012d8f28e5d3b269865038c346802f36f3da7278b13ad6a3bb6a1452e24ebeeaa24ba40eef559b1b287d2a2f80b7
-Password:
-Password (again):
-Old password:
-Old password (again):
-Added to key to device /dev/sda2, slot: 2
-</programlisting>
-    <para>
-      To ensure that this file system is decrypted using the FIDO2
-      compatible key, add the following to
-      <literal>configuration.nix</literal>:
-    </para>
-    <programlisting language="bash">
-boot.initrd.luks.fido2Support = true;
-boot.initrd.luks.devices.&quot;/dev/sda2&quot;.fido2.credential = &quot;f1d00200108b9d6e849a8b388da457688e3dd653b4e53770012d8f28e5d3b269865038c346802f36f3da7278b13ad6a3bb6a1452e24ebeeaa24ba40eef559b1b287d2a2f80b7&quot;;
-</programlisting>
-    <para>
-      You can also use the FIDO2 passwordless setup, but for security
-      reasons, you might want to enable it only when your device is PIN
-      protected, such as
-      <link xlink:href="https://trezor.io/">Trezor</link>.
-    </para>
-    <programlisting language="bash">
-boot.initrd.luks.devices.&quot;/dev/sda2&quot;.fido2.passwordLess = true;
-</programlisting>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/modularity.section.xml b/nixos/doc/manual/from_md/configuration/modularity.section.xml
deleted file mode 100644
index a7688090fcc..00000000000
--- a/nixos/doc/manual/from_md/configuration/modularity.section.xml
+++ /dev/null
@@ -1,152 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-modularity">
-  <title>Modularity</title>
-  <para>
-    The NixOS configuration mechanism is modular. If your
-    <literal>configuration.nix</literal> becomes too big, you can split
-    it into multiple files. Likewise, if you have multiple NixOS
-    configurations (e.g. for different computers) with some commonality,
-    you can move the common configuration into a shared file.
-  </para>
-  <para>
-    Modules have exactly the same syntax as
-    <literal>configuration.nix</literal>. In fact,
-    <literal>configuration.nix</literal> is itself a module. You can use
-    other modules by including them from
-    <literal>configuration.nix</literal>, e.g.:
-  </para>
-  <programlisting language="bash">
-{ config, pkgs, ... }:
-
-{ imports = [ ./vpn.nix ./kde.nix ];
-  services.httpd.enable = true;
-  environment.systemPackages = [ pkgs.emacs ];
-  ...
-}
-</programlisting>
-  <para>
-    Here, we include two modules from the same directory,
-    <literal>vpn.nix</literal> and <literal>kde.nix</literal>. The
-    latter might look like this:
-  </para>
-  <programlisting language="bash">
-{ config, pkgs, ... }:
-
-{ services.xserver.enable = true;
-  services.xserver.displayManager.sddm.enable = true;
-  services.xserver.desktopManager.plasma5.enable = true;
-  environment.systemPackages = [ pkgs.vim ];
-}
-</programlisting>
-  <para>
-    Note that both <literal>configuration.nix</literal> and
-    <literal>kde.nix</literal> define the option
-    <xref linkend="opt-environment.systemPackages" />. When multiple
-    modules define an option, NixOS will try to
-    <emphasis>merge</emphasis> the definitions. In the case of
-    <xref linkend="opt-environment.systemPackages" />, that’s easy: the
-    lists of packages can simply be concatenated. The value in
-    <literal>configuration.nix</literal> is merged last, so for
-    list-type options, it will appear at the end of the merged list. If
-    you want it to appear first, you can use
-    <literal>mkBefore</literal>:
-  </para>
-  <programlisting language="bash">
-boot.kernelModules = mkBefore [ &quot;kvm-intel&quot; ];
-</programlisting>
-  <para>
-    This causes the <literal>kvm-intel</literal> kernel module to be
-    loaded before any other kernel modules.
-  </para>
-  <para>
-    For other types of options, a merge may not be possible. For
-    instance, if two modules define
-    <xref linkend="opt-services.httpd.adminAddr" />,
-    <literal>nixos-rebuild</literal> will give an error:
-  </para>
-  <programlisting>
-The unique option `services.httpd.adminAddr' is defined multiple times, in `/etc/nixos/httpd.nix' and `/etc/nixos/configuration.nix'.
-</programlisting>
-  <para>
-    When that happens, it’s possible to force one definition take
-    precedence over the others:
-  </para>
-  <programlisting language="bash">
-services.httpd.adminAddr = pkgs.lib.mkForce &quot;bob@example.org&quot;;
-</programlisting>
-  <para>
-    When using multiple modules, you may need to access configuration
-    values defined in other modules. This is what the
-    <literal>config</literal> function argument is for: it contains the
-    complete, merged system configuration. That is,
-    <literal>config</literal> is the result of combining the
-    configurations returned by every module <footnote>
-      <para>
-        If you’re wondering how it’s possible that the (indirect)
-        <emphasis>result</emphasis> of a function is passed as an
-        <emphasis>input</emphasis> to that same function: that’s because
-        Nix is a <quote>lazy</quote> language — it only computes values
-        when they are needed. This works as long as no individual
-        configuration value depends on itself.
-      </para>
-    </footnote> . For example, here is a module that adds some packages
-    to <xref linkend="opt-environment.systemPackages" /> only if
-    <xref linkend="opt-services.xserver.enable" /> is set to
-    <literal>true</literal> somewhere else:
-  </para>
-  <programlisting language="bash">
-{ config, pkgs, ... }:
-
-{ environment.systemPackages =
-    if config.services.xserver.enable then
-      [ pkgs.firefox
-        pkgs.thunderbird
-      ]
-    else
-      [ ];
-}
-</programlisting>
-  <para>
-    With multiple modules, it may not be obvious what the final value of
-    a configuration option is. The command
-    <literal>nixos-option</literal> allows you to find out:
-  </para>
-  <programlisting>
-$ nixos-option services.xserver.enable
-true
-
-$ nixos-option boot.kernelModules
-[ &quot;tun&quot; &quot;ipv6&quot; &quot;loop&quot; ... ]
-</programlisting>
-  <para>
-    Interactive exploration of the configuration is possible using
-    <literal>nix repl</literal>, a read-eval-print loop for Nix
-    expressions. A typical use:
-  </para>
-  <programlisting>
-$ nix repl '&lt;nixpkgs/nixos&gt;'
-
-nix-repl&gt; config.networking.hostName
-&quot;mandark&quot;
-
-nix-repl&gt; map (x: x.hostName) config.services.httpd.virtualHosts
-[ &quot;example.org&quot; &quot;example.gov&quot; ]
-</programlisting>
-  <para>
-    While abstracting your configuration, you may find it useful to
-    generate modules using code, instead of writing files. The example
-    below would have the same effect as importing a file which sets
-    those options.
-  </para>
-  <programlisting language="bash">
-{ config, pkgs, ... }:
-
-let netConfig = hostName: {
-  networking.hostName = hostName;
-  networking.useDHCP = false;
-};
-
-in
-
-{ imports = [ (netConfig &quot;nixos.localdomain&quot;) ]; }
-</programlisting>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/network-manager.section.xml b/nixos/doc/manual/from_md/configuration/network-manager.section.xml
deleted file mode 100644
index 8f0d6d680ae..00000000000
--- a/nixos/doc/manual/from_md/configuration/network-manager.section.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-networkmanager">
-  <title>NetworkManager</title>
-  <para>
-    To facilitate network configuration, some desktop environments use
-    NetworkManager. You can enable NetworkManager by setting:
-  </para>
-  <programlisting language="bash">
-networking.networkmanager.enable = true;
-</programlisting>
-  <para>
-    some desktop managers (e.g., GNOME) enable NetworkManager
-    automatically for you.
-  </para>
-  <para>
-    All users that should have permission to change network settings
-    must belong to the <literal>networkmanager</literal> group:
-  </para>
-  <programlisting language="bash">
-users.users.alice.extraGroups = [ &quot;networkmanager&quot; ];
-</programlisting>
-  <para>
-    NetworkManager is controlled using either <literal>nmcli</literal>
-    or <literal>nmtui</literal> (curses-based terminal user interface).
-    See their manual pages for details on their usage. Some desktop
-    environments (GNOME, KDE) have their own configuration tools for
-    NetworkManager. On XFCE, there is no configuration tool for
-    NetworkManager by default: by enabling
-    <xref linkend="opt-programs.nm-applet.enable" />, the graphical
-    applet will be installed and will launch automatically when the
-    graphical session is started.
-  </para>
-  <note>
-    <para>
-      <literal>networking.networkmanager</literal> and
-      <literal>networking.wireless</literal> (WPA Supplicant) can be
-      used together if desired. To do this you need to instruct
-      NetworkManager to ignore those interfaces like:
-    </para>
-    <programlisting language="bash">
-networking.networkmanager.unmanaged = [
-   &quot;*&quot; &quot;except:type:wwan&quot; &quot;except:type:gsm&quot;
-];
-</programlisting>
-    <para>
-      Refer to the option description for the exact syntax and
-      references to external documentation.
-    </para>
-  </note>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/networking.chapter.xml b/nixos/doc/manual/from_md/configuration/networking.chapter.xml
deleted file mode 100644
index 2ed86ea3b58..00000000000
--- a/nixos/doc/manual/from_md/configuration/networking.chapter.xml
+++ /dev/null
@@ -1,15 +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" xml:id="sec-networking">
-  <title>Networking</title>
-  <para>
-    This section describes how to configure networking components on
-    your NixOS machine.
-  </para>
-  <xi:include href="network-manager.section.xml" />
-  <xi:include href="ssh.section.xml" />
-  <xi:include href="ipv4-config.section.xml" />
-  <xi:include href="ipv6-config.section.xml" />
-  <xi:include href="firewall.section.xml" />
-  <xi:include href="wireless.section.xml" />
-  <xi:include href="ad-hoc-network-config.section.xml" />
-  <xi:include href="renaming-interfaces.section.xml" />
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/package-mgmt.chapter.xml b/nixos/doc/manual/from_md/configuration/package-mgmt.chapter.xml
deleted file mode 100644
index d3727edbe08..00000000000
--- a/nixos/doc/manual/from_md/configuration/package-mgmt.chapter.xml
+++ /dev/null
@@ -1,28 +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" xml:id="sec-package-management">
-  <title>Package Management</title>
-  <para>
-    This section describes how to add additional packages to your
-    system. NixOS has two distinct styles of package management:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        <emphasis>Declarative</emphasis>, where you declare what
-        packages you want in your <literal>configuration.nix</literal>.
-        Every time you run <literal>nixos-rebuild</literal>, NixOS will
-        ensure that you get a consistent set of binaries corresponding
-        to your specification.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <emphasis>Ad hoc</emphasis>, where you install, upgrade and
-        uninstall packages via the <literal>nix-env</literal> command.
-        This style allows mixing packages from different Nixpkgs
-        versions. It’s the only choice for non-root users.
-      </para>
-    </listitem>
-  </itemizedlist>
-  <xi:include href="declarative-packages.section.xml" />
-  <xi:include href="ad-hoc-packages.section.xml" />
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/profiles.chapter.xml b/nixos/doc/manual/from_md/configuration/profiles.chapter.xml
deleted file mode 100644
index 6f5fc130c6a..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles.chapter.xml
+++ /dev/null
@@ -1,38 +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" xml:id="ch-profiles">
-  <title>Profiles</title>
-  <para>
-    In some cases, it may be desirable to take advantage of
-    commonly-used, predefined configurations provided by nixpkgs, but
-    different from those that come as default. This is a role fulfilled
-    by NixOS's Profiles, which come as files living in
-    <literal>&lt;nixpkgs/nixos/modules/profiles&gt;</literal>. That is
-    to say, expected usage is to add them to the imports list of your
-    <literal>/etc/configuration.nix</literal> as such:
-  </para>
-  <programlisting language="bash">
-imports = [
-  &lt;nixpkgs/nixos/modules/profiles/profile-name.nix&gt;
-];
-</programlisting>
-  <para>
-    Even if some of these profiles seem only useful in the context of
-    install media, many are actually intended to be used in real
-    installs.
-  </para>
-  <para>
-    What follows is a brief explanation on the purpose and use-case for
-    each profile. Detailing each option configured by each one is out of
-    scope.
-  </para>
-  <xi:include href="profiles/all-hardware.section.xml" />
-  <xi:include href="profiles/base.section.xml" />
-  <xi:include href="profiles/clone-config.section.xml" />
-  <xi:include href="profiles/demo.section.xml" />
-  <xi:include href="profiles/docker-container.section.xml" />
-  <xi:include href="profiles/graphical.section.xml" />
-  <xi:include href="profiles/hardened.section.xml" />
-  <xi:include href="profiles/headless.section.xml" />
-  <xi:include href="profiles/installation-device.section.xml" />
-  <xi:include href="profiles/minimal.section.xml" />
-  <xi:include href="profiles/qemu-guest.section.xml" />
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/all-hardware.section.xml b/nixos/doc/manual/from_md/configuration/profiles/all-hardware.section.xml
deleted file mode 100644
index 43ac5edea7f..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/all-hardware.section.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-all-hardware">
-  <title>All Hardware</title>
-  <para>
-    Enables all hardware supported by NixOS: i.e., all firmware is
-    included, and all devices from which one may boot are enabled in the
-    initrd. Its primary use is in the NixOS installation CDs.
-  </para>
-  <para>
-    The enabled kernel modules include support for SATA and PATA, SCSI
-    (partially), USB, Firewire (untested), Virtio (QEMU, KVM, etc.),
-    VMware, and Hyper-V. Additionally,
-    <xref linkend="opt-hardware.enableAllFirmware" /> is enabled, and
-    the firmware for the ZyDAS ZD1211 chipset is specifically installed.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/base.section.xml b/nixos/doc/manual/from_md/configuration/profiles/base.section.xml
deleted file mode 100644
index 83d35bd2867..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/base.section.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-base">
-  <title>Base</title>
-  <para>
-    Defines the software packages included in the <quote>minimal</quote>
-    installation CD. It installs several utilities useful in a simple
-    recovery or install media, such as a text-mode web browser, and
-    tools for manipulating block devices, networking, hardware
-    diagnostics, and filesystems (with their respective kernel modules).
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/clone-config.section.xml b/nixos/doc/manual/from_md/configuration/profiles/clone-config.section.xml
deleted file mode 100644
index 9430b49ea33..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/clone-config.section.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-clone-config">
-  <title>Clone Config</title>
-  <para>
-    This profile is used in installer images. It provides an editable
-    configuration.nix that imports all the modules that were also used
-    when creating the image in the first place. As a result it allows
-    users to edit and rebuild the live-system.
-  </para>
-  <para>
-    On images where the installation media also becomes an installation
-    target, copying over <literal>configuration.nix</literal> should be
-    disabled by setting <literal>installer.cloneConfig</literal> to
-    <literal>false</literal>. For example, this is done in
-    <literal>sd-image-aarch64-installer.nix</literal>.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/demo.section.xml b/nixos/doc/manual/from_md/configuration/profiles/demo.section.xml
deleted file mode 100644
index 09c2680a106..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/demo.section.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-demo">
-  <title>Demo</title>
-  <para>
-    This profile just enables a <literal>demo</literal> user, with
-    password <literal>demo</literal>, uid <literal>1000</literal>,
-    <literal>wheel</literal> group and
-    <link linkend="opt-services.xserver.displayManager.autoLogin">autologin
-    in the SDDM display manager</link>.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/docker-container.section.xml b/nixos/doc/manual/from_md/configuration/profiles/docker-container.section.xml
deleted file mode 100644
index 97c2a92dcab..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/docker-container.section.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-docker-container">
-  <title>Docker Container</title>
-  <para>
-    This is the profile from which the Docker images are generated. It
-    prepares a working system by importing the
-    <link linkend="sec-profile-minimal">Minimal</link> and
-    <link linkend="sec-profile-clone-config">Clone Config</link>
-    profiles, and setting appropriate configuration options that are
-    useful inside a container context, like
-    <xref linkend="opt-boot.isContainer" />.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/graphical.section.xml b/nixos/doc/manual/from_md/configuration/profiles/graphical.section.xml
deleted file mode 100644
index 1b109519d43..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/graphical.section.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-graphical">
-  <title>Graphical</title>
-  <para>
-    Defines a NixOS configuration with the Plasma 5 desktop. It’s used
-    by the graphical installation CD.
-  </para>
-  <para>
-    It sets <xref linkend="opt-services.xserver.enable" />,
-    <xref linkend="opt-services.xserver.displayManager.sddm.enable" />,
-    <xref linkend="opt-services.xserver.desktopManager.plasma5.enable" />,
-    and <xref linkend="opt-services.xserver.libinput.enable" /> to true.
-    It also includes glxinfo and firefox in the system packages list.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/hardened.section.xml b/nixos/doc/manual/from_md/configuration/profiles/hardened.section.xml
deleted file mode 100644
index 1fd5a917988..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/hardened.section.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-hardened">
-  <title>Hardened</title>
-  <para>
-    A profile with most (vanilla) hardening options enabled by default,
-    potentially at the cost of stability, features and performance.
-  </para>
-  <para>
-    This includes a hardened kernel, and limiting the system information
-    available to processes through the <literal>/sys</literal> and
-    <literal>/proc</literal> filesystems. It also disables the User
-    Namespaces feature of the kernel, which stops Nix from being able to
-    build anything (this particular setting can be overridden via
-    <xref linkend="opt-security.allowUserNamespaces" />). See the
-    <link xlink:href="https://github.com/nixos/nixpkgs/tree/master/nixos/modules/profiles/hardened.nix">profile
-    source</link> for further detail on which settings are altered.
-  </para>
-  <warning>
-    <para>
-      This profile enables options that are known to affect system
-      stability. If you experience any stability issues when using the
-      profile, try disabling it. If you report an issue and use this
-      profile, always mention that you do.
-    </para>
-  </warning>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/headless.section.xml b/nixos/doc/manual/from_md/configuration/profiles/headless.section.xml
deleted file mode 100644
index 0910b9ffaad..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/headless.section.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-headless">
-  <title>Headless</title>
-  <para>
-    Common configuration for headless machines (e.g., Amazon EC2
-    instances).
-  </para>
-  <para>
-    Disables <link linkend="opt-sound.enable">sound</link>,
-    <link linkend="opt-boot.vesa">vesa</link>, serial consoles,
-    <link linkend="opt-systemd.enableEmergencyMode">emergency
-    mode</link>, <link linkend="opt-boot.loader.grub.splashImage">grub
-    splash images</link> and configures the kernel to reboot
-    automatically on panic.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/installation-device.section.xml b/nixos/doc/manual/from_md/configuration/profiles/installation-device.section.xml
deleted file mode 100644
index 837e69df06e..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/installation-device.section.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-installation-device">
-  <title>Installation Device</title>
-  <para>
-    Provides a basic configuration for installation devices like CDs.
-    This enables redistributable firmware, includes the
-    <link linkend="sec-profile-clone-config">Clone Config profile</link>
-    and a copy of the Nixpkgs channel, so
-    <literal>nixos-install</literal> works out of the box.
-  </para>
-  <para>
-    Documentation for
-    <link linkend="opt-documentation.enable">Nixpkgs</link> and
-    <link linkend="opt-documentation.nixos.enable">NixOS</link> are
-    forcefully enabled (to override the
-    <link linkend="sec-profile-minimal">Minimal profile</link>
-    preference); the NixOS manual is shown automatically on TTY 8,
-    udisks is disabled. Autologin is enabled as <literal>nixos</literal>
-    user, while passwordless login as both <literal>root</literal> and
-    <literal>nixos</literal> is possible. Passwordless
-    <literal>sudo</literal> is enabled too.
-    <link linkend="opt-networking.wireless.enable">wpa_supplicant</link>
-    is enabled, but configured to not autostart.
-  </para>
-  <para>
-    It is explained how to login, start the ssh server, and if
-    available, how to start the display manager.
-  </para>
-  <para>
-    Several settings are tweaked so that the installer has a better
-    chance of succeeding under low-memory environments.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/minimal.section.xml b/nixos/doc/manual/from_md/configuration/profiles/minimal.section.xml
deleted file mode 100644
index a3fe30357df..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/minimal.section.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-minimal">
-  <title>Minimal</title>
-  <para>
-    This profile defines a small NixOS configuration. It does not
-    contain any graphical stuff. It’s a very short file that enables
-    <link linkend="opt-environment.noXlibs">noXlibs</link>, sets
-    <xref linkend="opt-i18n.supportedLocales" /> to only support the
-    user-selected locale,
-    <link linkend="opt-documentation.enable">disables packages’
-    documentation</link>, and <link linkend="opt-sound.enable">disables
-    sound</link>.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/profiles/qemu-guest.section.xml b/nixos/doc/manual/from_md/configuration/profiles/qemu-guest.section.xml
deleted file mode 100644
index f33464f9db4..00000000000
--- a/nixos/doc/manual/from_md/configuration/profiles/qemu-guest.section.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-profile-qemu-guest">
-  <title>QEMU Guest</title>
-  <para>
-    This profile contains common configuration for virtual machines
-    running under QEMU (using virtio).
-  </para>
-  <para>
-    It makes virtio modules available on the initrd and sets the system
-    time from the hardware clock to work around a bug in qemu-kvm.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/renaming-interfaces.section.xml b/nixos/doc/manual/from_md/configuration/renaming-interfaces.section.xml
deleted file mode 100644
index 88c9e624c82..00000000000
--- a/nixos/doc/manual/from_md/configuration/renaming-interfaces.section.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-rename-ifs">
-  <title>Renaming network interfaces</title>
-  <para>
-    NixOS uses the udev
-    <link xlink:href="https://systemd.io/PREDICTABLE_INTERFACE_NAMES/">predictable
-    naming scheme</link> to assign names to network interfaces. This
-    means that by default cards are not given the traditional names like
-    <literal>eth0</literal> or <literal>eth1</literal>, whose order can
-    change unpredictably across reboots. Instead, relying on physical
-    locations and firmware information, the scheme produces names like
-    <literal>ens1</literal>, <literal>enp2s0</literal>, etc.
-  </para>
-  <para>
-    These names are predictable but less memorable and not necessarily
-    stable: for example installing new hardware or changing firmware
-    settings can result in a
-    <link xlink:href="https://github.com/systemd/systemd/issues/3715#issue-165347602">name
-    change</link>. If this is undesirable, for example if you have a
-    single ethernet card, you can revert to the traditional scheme by
-    setting
-    <xref linkend="opt-networking.usePredictableInterfaceNames" /> to
-    <literal>false</literal>.
-  </para>
-  <section xml:id="sec-custom-ifnames">
-    <title>Assigning custom names</title>
-    <para>
-      In case there are multiple interfaces of the same type, it’s
-      better to assign custom names based on the device hardware
-      address. For example, we assign the name <literal>wan</literal> to
-      the interface with MAC address
-      <literal>52:54:00:12:01:01</literal> using a netword link unit:
-    </para>
-    <programlisting language="bash">
-systemd.network.links.&quot;10-wan&quot; = {
-  matchConfig.PermanentMACAddress = &quot;52:54:00:12:01:01&quot;;
-  linkConfig.Name = &quot;wan&quot;;
-};
-</programlisting>
-    <para>
-      Note that links are directly read by udev, <emphasis>not
-      networkd</emphasis>, and will work even if networkd is disabled.
-    </para>
-    <para>
-      Alternatively, we can use a plain old udev rule:
-    </para>
-    <programlisting language="bash">
-services.udev.initrdRules = ''
-  SUBSYSTEM==&quot;net&quot;, ACTION==&quot;add&quot;, DRIVERS==&quot;?*&quot;, \
-  ATTR{address}==&quot;52:54:00:12:01:01&quot;, KERNEL==&quot;eth*&quot;, NAME=&quot;wan&quot;
-'';
-</programlisting>
-    <warning>
-      <para>
-        The rule must be installed in the initrd using
-        <literal>services.udev.initrdRules</literal>, not the usual
-        <literal>services.udev.extraRules</literal> option. This is to
-        avoid race conditions with other programs controlling the
-        interface.
-      </para>
-    </warning>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/ssh.section.xml b/nixos/doc/manual/from_md/configuration/ssh.section.xml
deleted file mode 100644
index 037418d8ea4..00000000000
--- a/nixos/doc/manual/from_md/configuration/ssh.section.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-ssh">
-  <title>Secure Shell Access</title>
-  <para>
-    Secure shell (SSH) access to your machine can be enabled by setting:
-  </para>
-  <programlisting language="bash">
-services.openssh.enable = true;
-</programlisting>
-  <para>
-    By default, root logins using a password are disallowed. They can be
-    disabled entirely by setting
-    <xref linkend="opt-services.openssh.permitRootLogin" /> to
-    <literal>&quot;no&quot;</literal>.
-  </para>
-  <para>
-    You can declaratively specify authorised RSA/DSA public keys for a
-    user as follows:
-  </para>
-  <programlisting language="bash">
-users.users.alice.openssh.authorizedKeys.keys =
-  [ &quot;ssh-dss AAAAB3NzaC1kc3MAAACBAPIkGWVEt4...&quot; ];
-</programlisting>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/sshfs-file-systems.section.xml b/nixos/doc/manual/from_md/configuration/sshfs-file-systems.section.xml
deleted file mode 100644
index 5d74712f35d..00000000000
--- a/nixos/doc/manual/from_md/configuration/sshfs-file-systems.section.xml
+++ /dev/null
@@ -1,139 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-sshfs-file-systems">
-  <title>SSHFS File Systems</title>
-  <para>
-    <link xlink:href="https://github.com/libfuse/sshfs">SSHFS</link> is
-    a
-    <link xlink:href="https://en.wikipedia.org/wiki/Filesystem_in_Userspace">FUSE</link>
-    filesystem that allows easy access to directories on a remote
-    machine using the SSH File Transfer Protocol (SFTP). It means that
-    if you have SSH access to a machine, no additional setup is needed
-    to mount a directory.
-  </para>
-  <section xml:id="sec-sshfs-interactive">
-    <title>Interactive mounting</title>
-    <para>
-      In NixOS, SSHFS is packaged as <package>sshfs</package>. Once
-      installed, mounting a directory interactively is simple as
-      running:
-    </para>
-    <programlisting>
-$ sshfs my-user@example.com:/my-dir /mnt/my-dir
-</programlisting>
-    <para>
-      Like any other FUSE file system, the directory is unmounted using:
-    </para>
-    <programlisting>
-$ fusermount -u /mnt/my-dir
-</programlisting>
-  </section>
-  <section xml:id="sec-sshfs-non-interactive">
-    <title>Non-interactive mounting</title>
-    <para>
-      Mounting non-interactively requires some precautions because
-      <literal>sshfs</literal> will run at boot and under a different
-      user (root). For obvious reason, you can’t input a password, so
-      public key authentication using an unencrypted key is needed. To
-      create a new key without a passphrase you can do:
-    </para>
-    <programlisting>
-$ ssh-keygen -t ed25519 -P '' -f example-key
-Generating public/private ed25519 key pair.
-Your identification has been saved in test-key
-Your public key has been saved in test-key.pub
-The key fingerprint is:
-SHA256:yjxl3UbTn31fLWeyLYTAKYJPRmzknjQZoyG8gSNEoIE my-user@workstation
-</programlisting>
-    <para>
-      To keep the key safe, change the ownership to
-      <literal>root:root</literal> and make sure the permissions are
-      <literal>600</literal>: OpenSSH normally refuses to use the key if
-      it’s not well-protected.
-    </para>
-    <para>
-      The file system can be configured in NixOS via the usual
-      <link linkend="opt-fileSystems">fileSystems</link> option. Here’s
-      a typical setup:
-    </para>
-    <programlisting language="bash">
-{
-  system.fsPackages = [ pkgs.sshfs ];
-
-  fileSystems.&quot;/mnt/my-dir&quot; = {
-    device = &quot;my-user@example.com:/my-dir/&quot;;
-    fsType = &quot;sshfs&quot;;
-    options =
-      [ # Filesystem options
-        &quot;allow_other&quot;          # for non-root access
-        &quot;_netdev&quot;              # this is a network fs
-        &quot;x-systemd.automount&quot;  # mount on demand
-
-        # SSH options
-        &quot;reconnect&quot;              # handle connection drops
-        &quot;ServerAliveInterval=15&quot; # keep connections alive
-        &quot;IdentityFile=/var/secrets/example-key&quot;
-      ];
-  };
-}
-</programlisting>
-    <para>
-      More options from <literal>ssh_config(5)</literal> can be given as
-      well, for example you can change the default SSH port or specify a
-      jump proxy:
-    </para>
-    <programlisting language="bash">
-{
-  options =
-    [ &quot;ProxyJump=bastion@example.com&quot;
-      &quot;Port=22&quot;
-    ];
-}
-</programlisting>
-    <para>
-      It’s also possible to change the <literal>ssh</literal> command
-      used by SSHFS to connect to the server. For example:
-    </para>
-    <programlisting language="bash">
-{
-  options =
-    [ (builtins.replaceStrings [&quot; &quot;] [&quot;\\040&quot;]
-        &quot;ssh_command=${pkgs.openssh}/bin/ssh -v -L 8080:localhost:80&quot;)
-    ];
-
-}
-</programlisting>
-    <note>
-      <para>
-        The escaping of spaces is needed because every option is written
-        to the <literal>/etc/fstab</literal> file, which is a
-        space-separated table.
-      </para>
-    </note>
-    <section xml:id="sec-sshfs-troubleshooting">
-      <title>Troubleshooting</title>
-      <para>
-        If you’re having a hard time figuring out why mounting is
-        failing, you can add the option
-        <literal>&quot;debug&quot;</literal>. This enables a verbose log
-        in SSHFS that you can access via:
-      </para>
-      <programlisting>
-$ journalctl -u $(systemd-escape -p /mnt/my-dir/).mount
-Jun 22 11:41:18 workstation mount[87790]: SSHFS version 3.7.1
-Jun 22 11:41:18 workstation mount[87793]: executing &lt;ssh&gt; &lt;-x&gt; &lt;-a&gt; &lt;-oClearAllForwardings=yes&gt; &lt;-oServerAliveInterval=15&gt; &lt;-oIdentityFile=/var/secrets/wrong-key&gt; &lt;-2&gt; &lt;my-user@example.com&gt; &lt;-s&gt; &lt;sftp&gt;
-Jun 22 11:41:19 workstation mount[87793]: my-user@example.com: Permission denied (publickey).
-Jun 22 11:41:19 workstation mount[87790]: read: Connection reset by peer
-Jun 22 11:41:19 workstation systemd[1]: mnt-my\x2ddir.mount: Mount process exited, code=exited, status=1/FAILURE
-Jun 22 11:41:19 workstation systemd[1]: mnt-my\x2ddir.mount: Failed with result 'exit-code'.
-Jun 22 11:41:19 workstation systemd[1]: Failed to mount /mnt/my-dir.
-Jun 22 11:41:19 workstation systemd[1]: mnt-my\x2ddir.mount: Consumed 54ms CPU time, received 2.3K IP traffic, sent 2.7K IP traffic.
-</programlisting>
-      <note>
-        <para>
-          If the mount point contains special characters it needs to be
-          escaped using <literal>systemd-escape</literal>. This is due
-          to the way systemd converts paths into unit names.
-        </para>
-      </note>
-    </section>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/subversion.chapter.xml b/nixos/doc/manual/from_md/configuration/subversion.chapter.xml
deleted file mode 100644
index 794c2c34e39..00000000000
--- a/nixos/doc/manual/from_md/configuration/subversion.chapter.xml
+++ /dev/null
@@ -1,121 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-subversion">
-  <title>Subversion</title>
-  <para>
-    <link xlink:href="https://subversion.apache.org/">Subversion</link>
-    is a centralized version-control system. It can use a
-    <link xlink:href="http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.serverconfig.choosing">variety
-    of protocols</link> for communication between client and server.
-  </para>
-  <section xml:id="module-services-subversion-apache-httpd">
-    <title>Subversion inside Apache HTTP</title>
-    <para>
-      This section focuses on configuring a web-based server on top of
-      the Apache HTTP server, which uses
-      <link xlink:href="http://www.webdav.org/">WebDAV</link>/<link xlink:href="http://www.webdav.org/deltav/WWW10/deltav-intro.htm">DeltaV</link>
-      for communication.
-    </para>
-    <para>
-      For more information on the general setup, please refer to the
-      <link xlink:href="http://svnbook.red-bean.com/en/1.7/svn-book.html#svn.serverconfig.httpd">the
-      appropriate section of the Subversion book</link>.
-    </para>
-    <para>
-      To configure, include in
-      <literal>/etc/nixos/configuration.nix</literal> code to activate
-      Apache HTTP, setting
-      <xref linkend="opt-services.httpd.adminAddr" /> appropriately:
-    </para>
-    <programlisting language="bash">
-services.httpd.enable = true;
-services.httpd.adminAddr = ...;
-networking.firewall.allowedTCPPorts = [ 80 443 ];
-</programlisting>
-    <para>
-      For a simple Subversion server with basic authentication,
-      configure the Subversion module for Apache as follows, setting
-      <literal>hostName</literal> and <literal>documentRoot</literal>
-      appropriately, and <literal>SVNParentPath</literal> to the parent
-      directory of the repositories,
-      <literal>AuthzSVNAccessFile</literal> to the location of the
-      <literal>.authz</literal> file describing access permission, and
-      <literal>AuthUserFile</literal> to the password file.
-    </para>
-    <programlisting language="bash">
-services.httpd.extraModules = [
-    # note that order is *super* important here
-    { name = &quot;dav_svn&quot;; path = &quot;${pkgs.apacheHttpdPackages.subversion}/modules/mod_dav_svn.so&quot;; }
-    { name = &quot;authz_svn&quot;; path = &quot;${pkgs.apacheHttpdPackages.subversion}/modules/mod_authz_svn.so&quot;; }
-  ];
-  services.httpd.virtualHosts = {
-    &quot;svn&quot; = {
-       hostName = HOSTNAME;
-       documentRoot = DOCUMENTROOT;
-       locations.&quot;/svn&quot;.extraConfig = ''
-           DAV svn
-           SVNParentPath REPO_PARENT
-           AuthzSVNAccessFile ACCESS_FILE
-           AuthName &quot;SVN Repositories&quot;
-           AuthType Basic
-           AuthUserFile PASSWORD_FILE
-           Require valid-user
-      '';
-    }
-</programlisting>
-    <para>
-      The key <literal>&quot;svn&quot;</literal> is just a symbolic name
-      identifying the virtual host. The
-      <literal>&quot;/svn&quot;</literal> in
-      <literal>locations.&quot;/svn&quot;.extraConfig</literal> is the
-      path underneath which the repositories will be served.
-    </para>
-    <para>
-      <link xlink:href="https://wiki.archlinux.org/index.php/Subversion">This
-      page</link> explains how to set up the Subversion configuration
-      itself. This boils down to the following:
-    </para>
-    <para>
-      Underneath <literal>REPO_PARENT</literal> repositories can be set
-      up as follows:
-    </para>
-    <programlisting>
-$ svn create REPO_NAME
-</programlisting>
-    <para>
-      Repository files need to be accessible by
-      <literal>wwwrun</literal>:
-    </para>
-    <programlisting>
-$ chown -R wwwrun:wwwrun REPO_PARENT
-</programlisting>
-    <para>
-      The password file <literal>PASSWORD_FILE</literal> can be created
-      as follows:
-    </para>
-    <programlisting>
-$ htpasswd -cs PASSWORD_FILE USER_NAME
-</programlisting>
-    <para>
-      Additional users can be set up similarly, omitting the
-      <literal>c</literal> flag:
-    </para>
-    <programlisting>
-$ htpasswd -s PASSWORD_FILE USER_NAME
-</programlisting>
-    <para>
-      The file describing access permissions
-      <literal>ACCESS_FILE</literal> will look something like the
-      following:
-    </para>
-    <programlisting language="bash">
-[/]
-* = r
-
-[REPO_NAME:/]
-USER_NAME = rw
-</programlisting>
-    <para>
-      The Subversion repositories will be accessible as
-      <literal>http://HOSTNAME/svn/REPO_NAME</literal>.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/user-mgmt.chapter.xml b/nixos/doc/manual/from_md/configuration/user-mgmt.chapter.xml
deleted file mode 100644
index a2d7d2a9f11..00000000000
--- a/nixos/doc/manual/from_md/configuration/user-mgmt.chapter.xml
+++ /dev/null
@@ -1,105 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-user-management">
-  <title>User Management</title>
-  <para>
-    NixOS supports both declarative and imperative styles of user
-    management. In the declarative style, users are specified in
-    <literal>configuration.nix</literal>. For instance, the following
-    states that a user account named <literal>alice</literal> shall
-    exist:
-  </para>
-  <programlisting language="bash">
-users.users.alice = {
-  isNormalUser = true;
-  home = &quot;/home/alice&quot;;
-  description = &quot;Alice Foobar&quot;;
-  extraGroups = [ &quot;wheel&quot; &quot;networkmanager&quot; ];
-  openssh.authorizedKeys.keys = [ &quot;ssh-dss AAAAB3Nza... alice@foobar&quot; ];
-};
-</programlisting>
-  <para>
-    Note that <literal>alice</literal> is a member of the
-    <literal>wheel</literal> and <literal>networkmanager</literal>
-    groups, which allows her to use <literal>sudo</literal> to execute
-    commands as <literal>root</literal> and to configure the network,
-    respectively. Also note the SSH public key that allows remote logins
-    with the corresponding private key. Users created in this way do not
-    have a password by default, so they cannot log in via mechanisms
-    that require a password. However, you can use the
-    <literal>passwd</literal> program to set a password, which is
-    retained across invocations of <literal>nixos-rebuild</literal>.
-  </para>
-  <para>
-    If you set <xref linkend="opt-users.mutableUsers" /> to false, then
-    the contents of <literal>/etc/passwd</literal> and
-    <literal>/etc/group</literal> will be congruent to your NixOS
-    configuration. For instance, if you remove a user from
-    <xref linkend="opt-users.users" /> and run nixos-rebuild, the user
-    account will cease to exist. Also, imperative commands for managing
-    users and groups, such as useradd, are no longer available.
-    Passwords may still be assigned by setting the user's
-    <link linkend="opt-users.users._name_.hashedPassword">hashedPassword</link>
-    option. A hashed password can be generated using
-    <literal>mkpasswd</literal>.
-  </para>
-  <para>
-    A user ID (uid) is assigned automatically. You can also specify a
-    uid manually by adding
-  </para>
-  <programlisting language="bash">
-uid = 1000;
-</programlisting>
-  <para>
-    to the user specification.
-  </para>
-  <para>
-    Groups can be specified similarly. The following states that a group
-    named <literal>students</literal> shall exist:
-  </para>
-  <programlisting language="bash">
-users.groups.students.gid = 1000;
-</programlisting>
-  <para>
-    As with users, the group ID (gid) is optional and will be assigned
-    automatically if it’s missing.
-  </para>
-  <para>
-    In the imperative style, users and groups are managed by commands
-    such as <literal>useradd</literal>, <literal>groupmod</literal> and
-    so on. For instance, to create a user account named
-    <literal>alice</literal>:
-  </para>
-  <programlisting>
-# useradd -m alice
-</programlisting>
-  <para>
-    To make all nix tools available to this new user use `su - USER`
-    which opens a login shell (==shell that loads the profile) for given
-    user. This will create the ~/.nix-defexpr symlink. So run:
-  </para>
-  <programlisting>
-# su - alice -c &quot;true&quot;
-</programlisting>
-  <para>
-    The flag <literal>-m</literal> causes the creation of a home
-    directory for the new user, which is generally what you want. The
-    user does not have an initial password and therefore cannot log in.
-    A password can be set using the <literal>passwd</literal> utility:
-  </para>
-  <programlisting>
-# passwd alice
-Enter new UNIX password: ***
-Retype new UNIX password: ***
-</programlisting>
-  <para>
-    A user can be deleted using <literal>userdel</literal>:
-  </para>
-  <programlisting>
-# userdel -r alice
-</programlisting>
-  <para>
-    The flag <literal>-r</literal> deletes the user’s home directory.
-    Accounts can be modified using <literal>usermod</literal>. Unix
-    groups can be managed using <literal>groupadd</literal>,
-    <literal>groupmod</literal> and <literal>groupdel</literal>.
-  </para>
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/wayland.chapter.xml b/nixos/doc/manual/from_md/configuration/wayland.chapter.xml
deleted file mode 100644
index 1e90d4f3117..00000000000
--- a/nixos/doc/manual/from_md/configuration/wayland.chapter.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-wayland">
-  <title>Wayland</title>
-  <para>
-    While X11 (see <xref linkend="sec-x11" />) is still the primary
-    display technology on NixOS, Wayland support is steadily improving.
-    Where X11 separates the X Server and the window manager, on Wayland
-    those are combined: a Wayland Compositor is like an X11 window
-    manager, but also embeds the Wayland 'Server' functionality. This
-    means it is sufficient to install a Wayland Compositor such as sway
-    without separately enabling a Wayland server:
-  </para>
-  <programlisting language="bash">
-programs.sway.enable = true;
-</programlisting>
-  <para>
-    This installs the sway compositor along with some essential
-    utilities. Now you can start sway from the TTY console.
-  </para>
-  <para>
-    If you are using a wlroots-based compositor, like sway, and want to
-    be able to share your screen, you might want to activate this
-    option:
-  </para>
-  <programlisting language="bash">
-xdg.portal.wlr.enable = true;
-</programlisting>
-  <para>
-    and configure Pipewire using
-    <xref linkend="opt-services.pipewire.enable" /> and related options.
-  </para>
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/wireless.section.xml b/nixos/doc/manual/from_md/configuration/wireless.section.xml
deleted file mode 100644
index d39ec4fac49..00000000000
--- a/nixos/doc/manual/from_md/configuration/wireless.section.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-wireless">
-  <title>Wireless Networks</title>
-  <para>
-    For a desktop installation using NetworkManager (e.g., GNOME), you
-    just have to make sure the user is in the
-    <literal>networkmanager</literal> group and you can skip the rest of
-    this section on wireless networks.
-  </para>
-  <para>
-    NixOS will start wpa_supplicant for you if you enable this setting:
-  </para>
-  <programlisting language="bash">
-networking.wireless.enable = true;
-</programlisting>
-  <para>
-    NixOS lets you specify networks for wpa_supplicant declaratively:
-  </para>
-  <programlisting language="bash">
-networking.wireless.networks = {
-  echelon = {                # SSID with no spaces or special characters
-    psk = &quot;abcdefgh&quot;;
-  };
-  &quot;echelon's AP&quot; = {         # SSID with spaces and/or special characters
-    psk = &quot;ijklmnop&quot;;
-  };
-  echelon = {                # Hidden SSID
-    hidden = true;
-    psk = &quot;qrstuvwx&quot;;
-  };
-  free.wifi = {};            # Public wireless network
-};
-</programlisting>
-  <para>
-    Be aware that keys will be written to the nix store in plaintext!
-    When no networks are set, it will default to using a configuration
-    file at <literal>/etc/wpa_supplicant.conf</literal>. You should edit
-    this file yourself to define wireless networks, WPA keys and so on
-    (see wpa_supplicant.conf(5)).
-  </para>
-  <para>
-    If you are using WPA2 you can generate pskRaw key using
-    <literal>wpa_passphrase</literal>:
-  </para>
-  <programlisting>
-$ wpa_passphrase ESSID PSK
-network={
-        ssid=&quot;echelon&quot;
-        #psk=&quot;abcdefgh&quot;
-        psk=dca6d6ed41f4ab5a984c9f55f6f66d4efdc720ebf66959810f4329bb391c5435
-}
-</programlisting>
-  <programlisting language="bash">
-networking.wireless.networks = {
-  echelon = {
-    pskRaw = &quot;dca6d6ed41f4ab5a984c9f55f6f66d4efdc720ebf66959810f4329bb391c5435&quot;;
-  };
-};
-</programlisting>
-  <para>
-    or you can use it to directly generate the
-    <literal>wpa_supplicant.conf</literal>:
-  </para>
-  <programlisting>
-# wpa_passphrase ESSID PSK &gt; /etc/wpa_supplicant.conf
-</programlisting>
-  <para>
-    After you have edited the <literal>wpa_supplicant.conf</literal>,
-    you need to restart the wpa_supplicant service.
-  </para>
-  <programlisting>
-# systemctl restart wpa_supplicant.service
-</programlisting>
-</section>
diff --git a/nixos/doc/manual/from_md/configuration/x-windows.chapter.xml b/nixos/doc/manual/from_md/configuration/x-windows.chapter.xml
deleted file mode 100644
index c17e98983b2..00000000000
--- a/nixos/doc/manual/from_md/configuration/x-windows.chapter.xml
+++ /dev/null
@@ -1,380 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-x11">
-  <title>X Window System</title>
-  <para>
-    The X Window System (X11) provides the basis of NixOS’ graphical
-    user interface. It can be enabled as follows:
-  </para>
-  <programlisting language="bash">
-services.xserver.enable = true;
-</programlisting>
-  <para>
-    The X server will automatically detect and use the appropriate video
-    driver from a set of X.org drivers (such as <literal>vesa</literal>
-    and <literal>intel</literal>). You can also specify a driver
-    manually, e.g.
-  </para>
-  <programlisting language="bash">
-services.xserver.videoDrivers = [ &quot;r128&quot; ];
-</programlisting>
-  <para>
-    to enable X.org’s <literal>xf86-video-r128</literal> driver.
-  </para>
-  <para>
-    You also need to enable at least one desktop or window manager.
-    Otherwise, you can only log into a plain undecorated
-    <literal>xterm</literal> window. Thus you should pick one or more of
-    the following lines:
-  </para>
-  <programlisting language="bash">
-services.xserver.desktopManager.plasma5.enable = true;
-services.xserver.desktopManager.xfce.enable = true;
-services.xserver.desktopManager.gnome.enable = true;
-services.xserver.desktopManager.mate.enable = true;
-services.xserver.windowManager.xmonad.enable = true;
-services.xserver.windowManager.twm.enable = true;
-services.xserver.windowManager.icewm.enable = true;
-services.xserver.windowManager.i3.enable = true;
-services.xserver.windowManager.herbstluftwm.enable = true;
-</programlisting>
-  <para>
-    NixOS’s default <emphasis>display manager</emphasis> (the program
-    that provides a graphical login prompt and manages the X server) is
-    LightDM. You can select an alternative one by picking one of the
-    following lines:
-  </para>
-  <programlisting language="bash">
-services.xserver.displayManager.sddm.enable = true;
-services.xserver.displayManager.gdm.enable = true;
-</programlisting>
-  <para>
-    You can set the keyboard layout (and optionally the layout variant):
-  </para>
-  <programlisting language="bash">
-services.xserver.layout = &quot;de&quot;;
-services.xserver.xkbVariant = &quot;neo&quot;;
-</programlisting>
-  <para>
-    The X server is started automatically at boot time. If you don’t
-    want this to happen, you can set:
-  </para>
-  <programlisting language="bash">
-services.xserver.autorun = false;
-</programlisting>
-  <para>
-    The X server can then be started manually:
-  </para>
-  <programlisting>
-# systemctl start display-manager.service
-</programlisting>
-  <para>
-    On 64-bit systems, if you want OpenGL for 32-bit programs such as in
-    Wine, you should also set the following:
-  </para>
-  <programlisting language="bash">
-hardware.opengl.driSupport32Bit = true;
-</programlisting>
-  <section xml:id="sec-x11-auto-login">
-    <title>Auto-login</title>
-    <para>
-      The x11 login screen can be skipped entirely, automatically
-      logging you into your window manager and desktop environment when
-      you boot your computer.
-    </para>
-    <para>
-      This is especially helpful if you have disk encryption enabled.
-      Since you already have to provide a password to decrypt your disk,
-      entering a second password to login can be redundant.
-    </para>
-    <para>
-      To enable auto-login, you need to define your default window
-      manager and desktop environment. If you wanted no desktop
-      environment and i3 as your your window manager, you'd define:
-    </para>
-    <programlisting language="bash">
-services.xserver.displayManager.defaultSession = &quot;none+i3&quot;;
-</programlisting>
-    <para>
-      Every display manager in NixOS supports auto-login, here is an
-      example using lightdm for a user <literal>alice</literal>:
-    </para>
-    <programlisting language="bash">
-services.xserver.displayManager.lightdm.enable = true;
-services.xserver.displayManager.autoLogin.enable = true;
-services.xserver.displayManager.autoLogin.user = &quot;alice&quot;;
-</programlisting>
-  </section>
-  <section xml:id="sec-x11--graphics-cards-intel">
-    <title>Intel Graphics drivers</title>
-    <para>
-      There are two choices for Intel Graphics drivers in X.org:
-      <literal>modesetting</literal> (included in the xorg-server
-      itself) and <literal>intel</literal> (provided by the package
-      xf86-video-intel).
-    </para>
-    <para>
-      The default and recommended is <literal>modesetting</literal>. It
-      is a generic driver which uses the kernel
-      <link xlink:href="https://en.wikipedia.org/wiki/Mode_setting">mode
-      setting</link> (KMS) mechanism. It supports Glamor (2D graphics
-      acceleration via OpenGL) and is actively maintained but may
-      perform worse in some cases (like in old chipsets).
-    </para>
-    <para>
-      The second driver, <literal>intel</literal>, is specific to Intel
-      GPUs, but not recommended by most distributions: it lacks several
-      modern features (for example, it doesn't support Glamor) and the
-      package hasn't been officially updated since 2015.
-    </para>
-    <para>
-      The results vary depending on the hardware, so you may have to try
-      both drivers. Use the option
-      <xref linkend="opt-services.xserver.videoDrivers" /> to set one.
-      The recommended configuration for modern systems is:
-    </para>
-    <programlisting language="bash">
-services.xserver.videoDrivers = [ &quot;modesetting&quot; ];
-</programlisting>
-    <para>
-      If you experience screen tearing no matter what, this
-      configuration was reported to resolve the issue:
-    </para>
-    <programlisting language="bash">
-services.xserver.videoDrivers = [ &quot;intel&quot; ];
-services.xserver.deviceSection = ''
-  Option &quot;DRI&quot; &quot;2&quot;
-  Option &quot;TearFree&quot; &quot;true&quot;
-'';
-</programlisting>
-    <para>
-      Note that this will likely downgrade the performance compared to
-      <literal>modesetting</literal> or <literal>intel</literal> with
-      DRI 3 (default).
-    </para>
-  </section>
-  <section xml:id="sec-x11-graphics-cards-nvidia">
-    <title>Proprietary NVIDIA drivers</title>
-    <para>
-      NVIDIA provides a proprietary driver for its graphics cards that
-      has better 3D performance than the X.org drivers. It is not
-      enabled by default because it’s not free software. You can enable
-      it as follows:
-    </para>
-    <programlisting language="bash">
-services.xserver.videoDrivers = [ &quot;nvidia&quot; ];
-</programlisting>
-    <para>
-      Or if you have an older card, you may have to use one of the
-      legacy drivers:
-    </para>
-    <programlisting language="bash">
-services.xserver.videoDrivers = [ &quot;nvidiaLegacy390&quot; ];
-services.xserver.videoDrivers = [ &quot;nvidiaLegacy340&quot; ];
-services.xserver.videoDrivers = [ &quot;nvidiaLegacy304&quot; ];
-</programlisting>
-    <para>
-      You may need to reboot after enabling this driver to prevent a
-      clash with other kernel modules.
-    </para>
-  </section>
-  <section xml:id="sec-x11--graphics-cards-amd">
-    <title>Proprietary AMD drivers</title>
-    <para>
-      AMD provides a proprietary driver for its graphics cards that is
-      not enabled by default because it’s not Free Software, is often
-      broken in nixpkgs and as of this writing doesn't offer more
-      features or performance. If you still want to use it anyway, you
-      need to explicitly set:
-    </para>
-    <programlisting language="bash">
-services.xserver.videoDrivers = [ &quot;amdgpu-pro&quot; ];
-</programlisting>
-    <para>
-      You will need to reboot after enabling this driver to prevent a
-      clash with other kernel modules.
-    </para>
-  </section>
-  <section xml:id="sec-x11-touchpads">
-    <title>Touchpads</title>
-    <para>
-      Support for Synaptics touchpads (found in many laptops such as the
-      Dell Latitude series) can be enabled as follows:
-    </para>
-    <programlisting language="bash">
-services.xserver.libinput.enable = true;
-</programlisting>
-    <para>
-      The driver has many options (see <xref linkend="ch-options" />).
-      For instance, the following disables tap-to-click behavior:
-    </para>
-    <programlisting language="bash">
-services.xserver.libinput.touchpad.tapping = false;
-</programlisting>
-    <para>
-      Note: the use of <literal>services.xserver.synaptics</literal> is
-      deprecated since NixOS 17.09.
-    </para>
-  </section>
-  <section xml:id="sec-x11-gtk-and-qt-themes">
-    <title>GTK/Qt themes</title>
-    <para>
-      GTK themes can be installed either to user profile or system-wide
-      (via <literal>environment.systemPackages</literal>). To make Qt 5
-      applications look similar to GTK ones, you can use the following
-      configuration:
-    </para>
-    <programlisting language="bash">
-qt5.enable = true;
-qt5.platformTheme = &quot;gtk2&quot;;
-qt5.style = &quot;gtk2&quot;;
-</programlisting>
-  </section>
-  <section xml:id="custom-xkb-layouts">
-    <title>Custom XKB layouts</title>
-    <para>
-      It is possible to install custom
-      <link xlink:href="https://en.wikipedia.org/wiki/X_keyboard_extension">
-      XKB </link> keyboard layouts using the option
-      <literal>services.xserver.extraLayouts</literal>.
-    </para>
-    <para>
-      As a first example, we are going to create a layout based on the
-      basic US layout, with an additional layer to type some greek
-      symbols by pressing the right-alt key.
-    </para>
-    <para>
-      Create a file called <literal>us-greek</literal> with the
-      following content (under a directory called
-      <literal>symbols</literal>; it's an XKB peculiarity that will help
-      with testing):
-    </para>
-    <programlisting language="bash">
-xkb_symbols &quot;us-greek&quot;
-{
-  include &quot;us(basic)&quot;            // includes the base US keys
-  include &quot;level3(ralt_switch)&quot;  // configures right alt as a third level switch
-
-  key &lt;LatA&gt; { [ a, A, Greek_alpha ] };
-  key &lt;LatB&gt; { [ b, B, Greek_beta  ] };
-  key &lt;LatG&gt; { [ g, G, Greek_gamma ] };
-  key &lt;LatD&gt; { [ d, D, Greek_delta ] };
-  key &lt;LatZ&gt; { [ z, Z, Greek_zeta  ] };
-};
-</programlisting>
-    <para>
-      A minimal layout specification must include the following:
-    </para>
-    <programlisting language="bash">
-services.xserver.extraLayouts.us-greek = {
-  description = &quot;US layout with alt-gr greek&quot;;
-  languages   = [ &quot;eng&quot; ];
-  symbolsFile = /yourpath/symbols/us-greek;
-};
-</programlisting>
-    <note>
-      <para>
-        The name (after <literal>extraLayouts.</literal>) should match
-        the one given to the <literal>xkb_symbols</literal> block.
-      </para>
-    </note>
-    <para>
-      Applying this customization requires rebuilding several packages,
-      and a broken XKB file can lead to the X session crashing at login.
-      Therefore, you're strongly advised to <emphasis role="strong">test
-      your layout before applying it</emphasis>:
-    </para>
-    <programlisting>
-$ nix-shell -p xorg.xkbcomp
-$ setxkbmap -I/yourpath us-greek -print | xkbcomp -I/yourpath - $DISPLAY
-</programlisting>
-    <para>
-      You can inspect the predefined XKB files for examples:
-    </para>
-    <programlisting>
-$ echo &quot;$(nix-build --no-out-link '&lt;nixpkgs&gt;' -A xorg.xkeyboardconfig)/etc/X11/xkb/&quot;
-</programlisting>
-    <para>
-      Once the configuration is applied, and you did a logout/login
-      cycle, the layout should be ready to use. You can try it by e.g.
-      running <literal>setxkbmap us-greek</literal> and then type
-      <literal>&lt;alt&gt;+a</literal> (it may not get applied in your
-      terminal straight away). To change the default, the usual
-      <literal>services.xserver.layout</literal> option can still be
-      used.
-    </para>
-    <para>
-      A layout can have several other components besides
-      <literal>xkb_symbols</literal>, for example we will define new
-      keycodes for some multimedia key and bind these to some symbol.
-    </para>
-    <para>
-      Use the <emphasis>xev</emphasis> utility from
-      <literal>pkgs.xorg.xev</literal> to find the codes of the keys of
-      interest, then create a <literal>media-key</literal> file to hold
-      the keycodes definitions
-    </para>
-    <programlisting language="bash">
-xkb_keycodes &quot;media&quot;
-{
- &lt;volUp&gt;   = 123;
- &lt;volDown&gt; = 456;
-}
-</programlisting>
-    <para>
-      Now use the newly define keycodes in <literal>media-sym</literal>:
-    </para>
-    <programlisting language="bash">
-xkb_symbols &quot;media&quot;
-{
- key.type = &quot;ONE_LEVEL&quot;;
- key &lt;volUp&gt;   { [ XF86AudioLowerVolume ] };
- key &lt;volDown&gt; { [ XF86AudioRaiseVolume ] };
-}
-</programlisting>
-    <para>
-      As before, to install the layout do
-    </para>
-    <programlisting language="bash">
-services.xserver.extraLayouts.media = {
-  description  = &quot;Multimedia keys remapping&quot;;
-  languages    = [ &quot;eng&quot; ];
-  symbolsFile  = /path/to/media-key;
-  keycodesFile = /path/to/media-sym;
-};
-</programlisting>
-    <note>
-      <para>
-        The function
-        <literal>pkgs.writeText &lt;filename&gt; &lt;content&gt;</literal>
-        can be useful if you prefer to keep the layout definitions
-        inside the NixOS configuration.
-      </para>
-    </note>
-    <para>
-      Unfortunately, the Xorg server does not (currently) support
-      setting a keymap directly but relies instead on XKB rules to
-      select the matching components (keycodes, types, ...) of a layout.
-      This means that components other than symbols won't be loaded by
-      default. As a workaround, you can set the keymap using
-      <literal>setxkbmap</literal> at the start of the session with:
-    </para>
-    <programlisting language="bash">
-services.xserver.displayManager.sessionCommands = &quot;setxkbmap -keycodes media&quot;;
-</programlisting>
-    <para>
-      If you are manually starting the X server, you should set the
-      argument <literal>-xkbdir /etc/X11/xkb</literal>, otherwise X
-      won't find your layout files. For example with
-      <literal>xinit</literal> run
-    </para>
-    <programlisting>
-$ xinit -- -xkbdir /etc/X11/xkb
-</programlisting>
-    <para>
-      To learn how to write layouts take a look at the XKB
-      <link xlink:href="https://www.x.org/releases/current/doc/xorg-docs/input/XKB-Enhancing.html#Defining_New_Layouts">documentation
-      </link>. More example layouts can also be found
-      <link xlink:href="https://wiki.archlinux.org/index.php/X_KeyBoard_extension#Basic_examples">here
-      </link>.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/configuration/xfce.chapter.xml b/nixos/doc/manual/from_md/configuration/xfce.chapter.xml
deleted file mode 100644
index 42e70d1d81d..00000000000
--- a/nixos/doc/manual/from_md/configuration/xfce.chapter.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-xfce">
-  <title>Xfce Desktop Environment</title>
-  <para>
-    To enable the Xfce Desktop Environment, set
-  </para>
-  <programlisting language="bash">
-services.xserver.desktopManager.xfce.enable = true;
-services.xserver.displayManager.defaultSession = &quot;xfce&quot;;
-</programlisting>
-  <para>
-    Optionally, <emphasis>picom</emphasis> can be enabled for nice
-    graphical effects, some example settings:
-  </para>
-  <programlisting language="bash">
-services.picom = {
-  enable = true;
-  fade = true;
-  inactiveOpacity = 0.9;
-  shadow = true;
-  fadeDelta = 4;
-};
-</programlisting>
-  <para>
-    Some Xfce programs are not installed automatically. To install them
-    manually (system wide), put them into your
-    <xref linkend="opt-environment.systemPackages" /> from
-    <literal>pkgs.xfce</literal>.
-  </para>
-  <section xml:id="sec-xfce-thunar-plugins">
-    <title>Thunar</title>
-    <para>
-      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>
-    <para>
-      Even after enabling udisks2, volume management might not work.
-      Thunar and/or the desktop takes time to show up. Thunar will spit
-      out this kind of message on start (look at
-      <literal>journalctl --user -b</literal>).
-    </para>
-    <programlisting>
-Thunar:2410): GVFS-RemoteVolumeMonitor-WARNING **: remote volume monitor with dbus name org.gtk.Private.UDisks2VolumeMonitor is not supported
-</programlisting>
-    <para>
-      This is caused by some needed GNOME services not running. This is
-      all fixed by enabling &quot;Launch GNOME services on startup&quot;
-      in the Advanced tab of the Session and Startup settings panel.
-      Alternatively, you can run this command to do the same thing.
-    </para>
-    <programlisting>
-$ xfconf-query -c xfce4-session -p /compat/LaunchGNOME -s true
-</programlisting>
-    <para>
-      A log-out and re-log will be needed for this to take effect.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/contributing-to-this-manual.chapter.xml b/nixos/doc/manual/from_md/contributing-to-this-manual.chapter.xml
deleted file mode 100644
index 99dc5ce30b4..00000000000
--- a/nixos/doc/manual/from_md/contributing-to-this-manual.chapter.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="chap-contributing">
-  <title>Contributing to this manual</title>
-  <para>
-    The
-    <link xlink:href="https://en.wikipedia.org/wiki/DocBook">DocBook</link>
-    and CommonMark sources of the NixOS manual are in the
-    <link xlink:href="https://github.com/NixOS/nixpkgs/tree/master/nixos/doc/manual">nixos/doc/manual</link>
-    subdirectory of the
-    <link xlink:href="https://github.com/NixOS/nixpkgs">Nixpkgs</link>
-    repository.
-  </para>
-  <para>
-    You can quickly check your edits with the following:
-  </para>
-  <programlisting>
-$ cd /path/to/nixpkgs
-$ ./nixos/doc/manual/md-to-db.sh
-$ nix-build nixos/release.nix -A manual.x86_64-linux
-</programlisting>
-  <para>
-    If the build succeeds, the manual will be in
-    <literal>./result/share/doc/nixos/index.html</literal>.
-  </para>
-  <para>
-    <emphasis role="strong">Contributing to the man pages</emphasis>
-  </para>
-  <para>
-    The man pages are written in
-    <link xlink:href="https://en.wikipedia.org/wiki/DocBook">DocBook</link>
-    which is XML.
-  </para>
-  <para>
-    To see what your edits look like:
-  </para>
-  <programlisting>
-$ cd /path/to/nixpkgs
-$ nix-build nixos/release.nix -A manpages.x86_64-linux
-</programlisting>
-  <para>
-    You can then read the man page you edited by running
-  </para>
-  <programlisting>
-$ man --manpath=result/share/man nixos-rebuild # Replace nixos-rebuild with the command whose manual you edited
-</programlisting>
-  <para>
-    If you’re on a different architecture that’s supported by NixOS
-    (check nixos/release.nix) then replace
-    <literal>x86_64-linux</literal> with the architecture.
-    <literal>nix-build</literal> will complain otherwise, but should
-    also tell you which architecture you have + the supported ones.
-  </para>
-</chapter>
diff --git a/nixos/doc/manual/from_md/development/activation-script.section.xml b/nixos/doc/manual/from_md/development/activation-script.section.xml
deleted file mode 100644
index 8672ab8afe5..00000000000
--- a/nixos/doc/manual/from_md/development/activation-script.section.xml
+++ /dev/null
@@ -1,150 +0,0 @@
-<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
-    incorporate 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 possibility 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/assertions.section.xml b/nixos/doc/manual/from_md/development/assertions.section.xml
deleted file mode 100644
index 0844d484d60..00000000000
--- a/nixos/doc/manual/from_md/development/assertions.section.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-assertions">
-  <title>Warnings and Assertions</title>
-  <para>
-    When configuration problems are detectable in a module, it is a good
-    idea to write an assertion or warning. Doing so provides clear
-    feedback to the user and prevents errors after the build.
-  </para>
-  <para>
-    Although Nix has the <literal>abort</literal> and
-    <literal>builtins.trace</literal>
-    <link xlink:href="https://nixos.org/nix/manual/#ssec-builtins">functions</link>
-    to perform such tasks, they are not ideally suited for NixOS
-    modules. Instead of these functions, you can declare your warnings
-    and assertions using the NixOS module system.
-  </para>
-  <section xml:id="sec-assertions-warnings">
-    <title>Warnings</title>
-    <para>
-      This is an example of using <literal>warnings</literal>.
-    </para>
-    <programlisting language="bash">
-{ config, lib, ... }:
-{
-  config = lib.mkIf config.services.foo.enable {
-    warnings =
-      if config.services.foo.bar
-      then [ ''You have enabled the bar feature of the foo service.
-               This is known to cause some specific problems in certain situations.
-               '' ]
-      else [];
-  }
-}
-</programlisting>
-  </section>
-  <section xml:id="sec-assertions-assetions">
-    <title>Assertions</title>
-    <para>
-      This example, extracted from the
-      <link xlink:href="https://github.com/NixOS/nixpkgs/blob/release-17.09/nixos/modules/services/logging/syslogd.nix"><literal>syslogd</literal>
-      module</link> shows how to use <literal>assertions</literal>.
-      Since there can only be one active syslog daemon at a time, an
-      assertion is useful to prevent such a broken system from being
-      built.
-    </para>
-    <programlisting language="bash">
-{ config, lib, ... }:
-{
-  config = lib.mkIf config.services.syslogd.enable {
-    assertions =
-      [ { assertion = !config.services.rsyslogd.enable;
-          message = &quot;rsyslogd conflicts with syslogd&quot;;
-        }
-      ];
-  }
-}
-</programlisting>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/development/bootspec.chapter.xml b/nixos/doc/manual/from_md/development/bootspec.chapter.xml
deleted file mode 100644
index acf8ca76bf5..00000000000
--- a/nixos/doc/manual/from_md/development/bootspec.chapter.xml
+++ /dev/null
@@ -1,73 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-experimental-bootspec">
-  <title>Experimental feature: Bootspec</title>
-  <para>
-    Bootspec is a experimental feature, introduced in the
-    <link xlink:href="https://github.com/NixOS/rfcs/pull/125">RFC-0125
-    proposal</link>, the reference implementation can be found
-    <link xlink:href="https://github.com/NixOS/nixpkgs/pull/172237">there</link>
-    in order to standardize bootloader support and advanced boot
-    workflows such as SecureBoot and potentially more.
-  </para>
-  <para>
-    You can enable the creation of bootspec documents through
-    <link xlink:href="options.html#opt-boot.bootspec.enable"><literal>boot.bootspec.enable = true</literal></link>,
-    which will prompt a warning until
-    <link xlink:href="https://github.com/NixOS/rfcs/pull/125">RFC-0125</link>
-    is officially merged.
-  </para>
-  <section xml:id="sec-experimental-bootspec-schema">
-    <title>Schema</title>
-    <para>
-      The bootspec schema is versioned and validated against
-      <link xlink:href="https://cuelang.org/">a CUE schema file</link>
-      which should considered as the source of truth for your
-      applications.
-    </para>
-    <para>
-      You will find the current version
-      <link xlink:href="../../../modules/system/activation/bootspec.cue">here</link>.
-    </para>
-  </section>
-  <section xml:id="sec-experimental-bootspec-extensions">
-    <title>Extensions mechanism</title>
-    <para>
-      Bootspec cannot account for all usecases.
-    </para>
-    <para>
-      For this purpose, Bootspec offers a generic extension facility
-      <link xlink:href="options.html#opt-boot.bootspec.extensions"><literal>boot.bootspec.extensions</literal></link>
-      which can be used to inject any data needed for your usecases.
-    </para>
-    <para>
-      An example for SecureBoot is to get the Nix store path to
-      <literal>/etc/os-release</literal> in order to bake it into a
-      unified kernel image:
-    </para>
-    <programlisting language="bash">
-{ config, lib, ... }: {
-  boot.bootspec.extensions = {
-    &quot;org.secureboot.osRelease&quot; = config.environment.etc.&quot;os-release&quot;.source;
-  };
-}
-</programlisting>
-    <para>
-      To reduce incompatibility and prevent names from clashing between
-      applications, it is <emphasis role="strong">highly
-      recommended</emphasis> to use a unique namespace for your
-      extensions.
-    </para>
-  </section>
-  <section xml:id="sec-experimental-bootspec-external-bootloaders">
-    <title>External bootloaders</title>
-    <para>
-      It is possible to enable your own bootloader through
-      <link xlink:href="options.html#opt-boot.loader.external.installHook"><literal>boot.loader.external.installHook</literal></link>
-      which can wrap an existing bootloader.
-    </para>
-    <para>
-      Currently, there is no good story to compose existing bootloaders
-      to enrich their features, e.g. SecureBoot, etc. It will be
-      necessary to reimplement or reuse existing parts.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/development/building-parts.chapter.xml b/nixos/doc/manual/from_md/development/building-parts.chapter.xml
deleted file mode 100644
index 4df24cc9565..00000000000
--- a/nixos/doc/manual/from_md/development/building-parts.chapter.xml
+++ /dev/null
@@ -1,124 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-building-parts">
-  <title>Building Specific Parts of NixOS</title>
-  <para>
-    With the command <literal>nix-build</literal>, you can build
-    specific parts of your NixOS configuration. This is done as follows:
-  </para>
-  <programlisting>
-$ cd /path/to/nixpkgs/nixos
-$ nix-build -A config.option
-</programlisting>
-  <para>
-    where <literal>option</literal> is a NixOS option with type
-    <quote>derivation</quote> (i.e. something that can be built).
-    Attributes of interest include:
-  </para>
-  <variablelist>
-    <varlistentry>
-      <term>
-        <literal>system.build.toplevel</literal>
-      </term>
-      <listitem>
-        <para>
-          The top-level option that builds the entire NixOS system.
-          Everything else in your configuration is indirectly pulled in
-          by this option. This is what <literal>nixos-rebuild</literal>
-          builds and what <literal>/run/current-system</literal> points
-          to afterwards.
-        </para>
-        <para>
-          A shortcut to build this is:
-        </para>
-        <programlisting>
-$ nix-build -A system
-</programlisting>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>system.build.manual.manualHTML</literal>
-      </term>
-      <listitem>
-        <para>
-          The NixOS manual.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>system.build.etc</literal>
-      </term>
-      <listitem>
-        <para>
-          A tree of symlinks that form the static parts of
-          <literal>/etc</literal>.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>system.build.initialRamdisk</literal> ,
-        <literal>system.build.kernel</literal>
-      </term>
-      <listitem>
-        <para>
-          The initial ramdisk and kernel of the system. This allows a
-          quick way to test whether the kernel and the initial ramdisk
-          boot correctly, by using QEMU’s <literal>-kernel</literal> and
-          <literal>-initrd</literal> options:
-        </para>
-        <programlisting>
-$ nix-build -A config.system.build.initialRamdisk -o initrd
-$ nix-build -A config.system.build.kernel -o kernel
-$ qemu-system-x86_64 -kernel ./kernel/bzImage -initrd ./initrd/initrd -hda /dev/null
-</programlisting>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>system.build.nixos-rebuild</literal> ,
-        <literal>system.build.nixos-install</literal> ,
-        <literal>system.build.nixos-generate-config</literal>
-      </term>
-      <listitem>
-        <para>
-          These build the corresponding NixOS commands.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>systemd.units.unit-name.unit</literal>
-      </term>
-      <listitem>
-        <para>
-          This builds the unit with the specified name. Note that since
-          unit names contain dots (e.g.
-          <literal>httpd.service</literal>), you need to put them
-          between quotes, like this:
-        </para>
-        <programlisting>
-$ nix-build -A 'config.systemd.units.&quot;httpd.service&quot;.unit'
-</programlisting>
-        <para>
-          You can also test individual units, without rebuilding the
-          whole system, by putting them in
-          <literal>/run/systemd/system</literal>:
-        </para>
-        <programlisting>
-$ cp $(nix-build -A 'config.systemd.units.&quot;httpd.service&quot;.unit')/httpd.service \
-    /run/systemd/system/tmp-httpd.service
-# systemctl daemon-reload
-# systemctl start tmp-httpd.service
-</programlisting>
-        <para>
-          Note that the unit must not have the same name as any unit in
-          <literal>/etc/systemd/system</literal> since those take
-          precedence over <literal>/run/systemd/system</literal>. That’s
-          why the unit is installed as
-          <literal>tmp-httpd.service</literal> here.
-        </para>
-      </listitem>
-    </varlistentry>
-  </variablelist>
-</chapter>
diff --git a/nixos/doc/manual/from_md/development/freeform-modules.section.xml b/nixos/doc/manual/from_md/development/freeform-modules.section.xml
deleted file mode 100644
index 86a9cf3140d..00000000000
--- a/nixos/doc/manual/from_md/development/freeform-modules.section.xml
+++ /dev/null
@@ -1,87 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-freeform-modules">
-  <title>Freeform modules</title>
-  <para>
-    Freeform modules allow you to define values for option paths that
-    have not been declared explicitly. This can be used to add
-    attribute-specific types to what would otherwise have to be
-    <literal>attrsOf</literal> options in order to accept all attribute
-    names.
-  </para>
-  <para>
-    This feature can be enabled by using the attribute
-    <literal>freeformType</literal> to define a freeform type. By doing
-    this, all assignments without an associated option will be merged
-    using the freeform type and combined into the resulting
-    <literal>config</literal> set. Since this feature nullifies name
-    checking for entire option trees, it is only recommended for use in
-    submodules.
-  </para>
-  <anchor xml:id="ex-freeform-module" />
-  <para>
-    <emphasis role="strong">Example: Freeform submodule</emphasis>
-  </para>
-  <para>
-    The following shows a submodule assigning a freeform type that
-    allows arbitrary attributes with <literal>str</literal> values below
-    <literal>settings</literal>, but also declares an option for the
-    <literal>settings.port</literal> attribute to have it type-checked
-    and assign a default value. See
-    <link linkend="ex-settings-typed-attrs">Example: Declaring a
-    type-checked <literal>settings</literal> attribute</link> for a more
-    complete example.
-  </para>
-  <programlisting language="bash">
-{ lib, config, ... }: {
-
-  options.settings = lib.mkOption {
-    type = lib.types.submodule {
-
-      freeformType = with lib.types; attrsOf str;
-
-      # We want this attribute to be checked for the correct type
-      options.port = lib.mkOption {
-        type = lib.types.port;
-        # Declaring the option also allows defining a default value
-        default = 8080;
-      };
-
-    };
-  };
-}
-</programlisting>
-  <para>
-    And the following shows what such a module then allows
-  </para>
-  <programlisting language="bash">
-{
-  # Not a declared option, but the freeform type allows this
-  settings.logLevel = &quot;debug&quot;;
-
-  # Not allowed because the the freeform type only allows strings
-  # settings.enable = true;
-
-  # Allowed because there is a port option declared
-  settings.port = 80;
-
-  # Not allowed because the port option doesn't allow strings
-  # settings.port = &quot;443&quot;;
-}
-</programlisting>
-  <note>
-    <para>
-      Freeform attributes cannot depend on other attributes of the same
-      set without infinite recursion:
-    </para>
-    <programlisting language="bash">
-{
-  # This throws infinite recursion encountered
-  settings.logLevel = lib.mkIf (config.settings.port == 80) &quot;debug&quot;;
-}
-</programlisting>
-    <para>
-      To prevent this, declare options for all attributes that need to
-      depend on others. For above example this means to declare
-      <literal>logLevel</literal> to be an option.
-    </para>
-  </note>
-</section>
diff --git a/nixos/doc/manual/from_md/development/importing-modules.section.xml b/nixos/doc/manual/from_md/development/importing-modules.section.xml
deleted file mode 100644
index cb04dde67c8..00000000000
--- a/nixos/doc/manual/from_md/development/importing-modules.section.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-importing-modules">
-  <title>Importing Modules</title>
-  <para>
-    Sometimes NixOS modules need to be used in configuration but exist
-    outside of Nixpkgs. These modules can be imported:
-  </para>
-  <programlisting language="bash">
-{ config, lib, pkgs, ... }:
-
-{
-  imports =
-    [ # Use a locally-available module definition in
-      # ./example-module/default.nix
-        ./example-module
-    ];
-
-  services.exampleModule.enable = true;
-}
-</programlisting>
-  <para>
-    The environment variable <literal>NIXOS_EXTRA_MODULE_PATH</literal>
-    is an absolute path to a NixOS module that is included alongside the
-    Nixpkgs NixOS modules. Like any NixOS module, this module can import
-    additional modules:
-  </para>
-  <programlisting language="bash">
-# ./module-list/default.nix
-[
-  ./example-module1
-  ./example-module2
-]
-</programlisting>
-  <programlisting language="bash">
-# ./extra-module/default.nix
-{ imports = import ./module-list.nix; }
-</programlisting>
-  <programlisting language="bash">
-# NIXOS_EXTRA_MODULE_PATH=/absolute/path/to/extra-module
-{ config, lib, pkgs, ... }:
-
-{
-  # No `imports` needed
-
-  services.exampleModule1.enable = true;
-}
-</programlisting>
-</section>
diff --git a/nixos/doc/manual/from_md/development/linking-nixos-tests-to-packages.section.xml b/nixos/doc/manual/from_md/development/linking-nixos-tests-to-packages.section.xml
deleted file mode 100644
index 666bbec6162..00000000000
--- a/nixos/doc/manual/from_md/development/linking-nixos-tests-to-packages.section.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-linking-nixos-tests-to-packages">
-  <title>Linking NixOS tests to packages</title>
-  <para>
-    You can link NixOS module tests to the packages that they exercised,
-    so that the tests can be run automatically during code review when
-    the package gets changed. This is
-    <link xlink:href="https://nixos.org/manual/nixpkgs/stable/#ssec-nixos-tests-linking">described
-    in the nixpkgs manual</link>.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/development/meta-attributes.section.xml b/nixos/doc/manual/from_md/development/meta-attributes.section.xml
deleted file mode 100644
index 1eb6e0f3036..00000000000
--- a/nixos/doc/manual/from_md/development/meta-attributes.section.xml
+++ /dev/null
@@ -1,95 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-meta-attributes">
-  <title>Meta Attributes</title>
-  <para>
-    Like Nix packages, NixOS modules can declare meta-attributes to
-    provide extra information. Module meta attributes are defined in the
-    <literal>meta.nix</literal> special module.
-  </para>
-  <para>
-    <literal>meta</literal> is a top level attribute like
-    <literal>options</literal> and <literal>config</literal>. Available
-    meta-attributes are <literal>maintainers</literal>,
-    <literal>doc</literal>, and <literal>buildDocsInSandbox</literal>.
-  </para>
-  <para>
-    Each of the meta-attributes must be defined at most once per module
-    file.
-  </para>
-  <programlisting language="bash">
-{ config, lib, pkgs, ... }:
-{
-  options = {
-    ...
-  };
-
-  config = {
-    ...
-  };
-
-  meta = {
-    maintainers = with lib.maintainers; [ ericsagnes ];
-    doc = ./default.xml;
-    buildDocsInSandbox = true;
-  };
-}
-</programlisting>
-  <itemizedlist>
-    <listitem>
-      <para>
-        <literal>maintainers</literal> contains a list of the module
-        maintainers.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>doc</literal> points to a valid DocBook file containing
-        the module documentation. Its contents is automatically added to
-        <xref linkend="ch-configuration" />. Changes to a module
-        documentation have to be checked to not break building the NixOS
-        manual:
-      </para>
-      <programlisting>
-$ nix-build nixos/release.nix -A manual.x86_64-linux
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>buildDocsInSandbox</literal> indicates whether the
-        option documentation for the module can be built in a derivation
-        sandbox. This option is currently only honored for modules
-        shipped by nixpkgs. User modules and modules taken from
-        <literal>NIXOS_EXTRA_MODULE_PATH</literal> are always built
-        outside of the sandbox, as has been the case in previous
-        releases.
-      </para>
-      <para>
-        Building NixOS option documentation in a sandbox allows caching
-        of the built documentation, which greatly decreases the amount
-        of time needed to evaluate a system configuration that has NixOS
-        documentation enabled. The sandbox also restricts which
-        attributes may be referenced by documentation attributes (such
-        as option descriptions) to the <literal>options</literal> and
-        <literal>lib</literal> module arguments and the
-        <literal>pkgs.formats</literal> attribute of the
-        <literal>pkgs</literal> argument, <literal>config</literal> and
-        the rest of <literal>pkgs</literal> are disallowed and will
-        cause doc build failures when used. This restriction is
-        necessary because we cannot reproduce the full nixpkgs
-        instantiation with configuration and overlays from a system
-        configuration inside the sandbox. The <literal>options</literal>
-        argument only includes options of modules that are also built
-        inside the sandbox, referencing an option of a module that isn’t
-        built in the sandbox is also forbidden.
-      </para>
-      <para>
-        The default is <literal>true</literal> and should usually not be
-        changed; set it to <literal>false</literal> only if the module
-        requires access to <literal>pkgs</literal> in its documentation
-        (e.g. because it loads information from a linked package to
-        build an option type) or if its documentation depends on other
-        modules that also aren’t sandboxed (e.g. by using types defined
-        in the other module).
-      </para>
-    </listitem>
-  </itemizedlist>
-</section>
diff --git a/nixos/doc/manual/from_md/development/nixos-tests.chapter.xml b/nixos/doc/manual/from_md/development/nixos-tests.chapter.xml
deleted file mode 100644
index b9ff2269676..00000000000
--- a/nixos/doc/manual/from_md/development/nixos-tests.chapter.xml
+++ /dev/null
@@ -1,14 +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" xml:id="sec-nixos-tests">
-  <title>NixOS Tests</title>
-  <para>
-    When you add some feature to NixOS, you should write a test for it.
-    NixOS tests are kept in the directory
-    <literal>nixos/tests</literal>, and are executed (using Nix) by a
-    testing framework that automatically starts one or more virtual
-    machines containing the NixOS system(s) required for the test.
-  </para>
-  <xi:include href="writing-nixos-tests.section.xml" />
-  <xi:include href="running-nixos-tests.section.xml" />
-  <xi:include href="running-nixos-tests-interactively.section.xml" />
-  <xi:include href="linking-nixos-tests-to-packages.section.xml" />
-</chapter>
diff --git a/nixos/doc/manual/from_md/development/option-declarations.section.xml b/nixos/doc/manual/from_md/development/option-declarations.section.xml
deleted file mode 100644
index 0932a51a18c..00000000000
--- a/nixos/doc/manual/from_md/development/option-declarations.section.xml
+++ /dev/null
@@ -1,335 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-option-declarations">
-  <title>Option Declarations</title>
-  <para>
-    An option declaration specifies the name, type and description of a
-    NixOS configuration option. It is invalid to define an option that
-    hasn’t been declared in any module. An option declaration generally
-    looks like this:
-  </para>
-  <programlisting language="bash">
-options = {
-  name = mkOption {
-    type = type specification;
-    default = default value;
-    example = example value;
-    description = lib.mdDoc &quot;Description for use in the NixOS manual.&quot;;
-  };
-};
-</programlisting>
-  <para>
-    The attribute names within the <literal>name</literal> attribute
-    path must be camel cased in general but should, as an exception,
-    match the
-    <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-package-naming">
-    package attribute name</link> when referencing a Nixpkgs package.
-    For example, the option
-    <literal>services.nix-serve.bindAddress</literal> references the
-    <literal>nix-serve</literal> Nixpkgs package.
-  </para>
-  <para>
-    The function <literal>mkOption</literal> accepts the following
-    arguments.
-  </para>
-  <variablelist>
-    <varlistentry>
-      <term>
-        <literal>type</literal>
-      </term>
-      <listitem>
-        <para>
-          The type of the option (see
-          <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>
-    <varlistentry>
-      <term>
-        <literal>default</literal>
-      </term>
-      <listitem>
-        <para>
-          The default value used if no value is defined by any module. A
-          default is not required; but if a default is not given, then
-          users of the module will have to define the value of the
-          option, otherwise an error will be thrown.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>defaultText</literal>
-      </term>
-      <listitem>
-        <para>
-          A textual representation of the default value to be rendered
-          verbatim in the manual. Useful if the default value is a
-          complex expression or depends on other values or packages. Use
-          <literal>lib.literalExpression</literal> for a Nix expression,
-          <literal>lib.literalMD</literal> for a plain English
-          description in
-          <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-contributing-markup">Nixpkgs-flavored
-          Markdown</link> format.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>example</literal>
-      </term>
-      <listitem>
-        <para>
-          An example value that will be shown in the NixOS manual. You
-          can use <literal>lib.literalExpression</literal> and
-          <literal>lib.literalMD</literal> in the same way as in
-          <literal>defaultText</literal>.
-        </para>
-      </listitem>
-    </varlistentry>
-    <varlistentry>
-      <term>
-        <literal>description</literal>
-      </term>
-      <listitem>
-        <para>
-          A textual description of the option, in
-          <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-contributing-markup">Nixpkgs-flavored
-          Markdown</link> format, that will be included in the NixOS
-          manual. During the migration process from DocBook it is
-          necessary to mark descriptions written in CommonMark with
-          <literal>lib.mdDoc</literal>. The description may still be
-          written in DocBook (without any marker), but this is
-          discouraged and will be deprecated in the future.
-        </para>
-      </listitem>
-    </varlistentry>
-  </variablelist>
-  <section xml:id="sec-option-declarations-util">
-    <title>Utility functions for common option patterns</title>
-    <section xml:id="sec-option-declarations-util-mkEnableOption">
-      <title><literal>mkEnableOption</literal></title>
-      <para>
-        Creates an Option attribute set for a boolean value option i.e
-        an option to be toggled on or off.
-      </para>
-      <para>
-        This function takes a single string argument, the name of the
-        thing to be toggled.
-      </para>
-      <para>
-        The option’s description is <quote>Whether to enable
-        &lt;name&gt;.</quote>.
-      </para>
-      <para>
-        For example:
-      </para>
-      <anchor xml:id="ex-options-declarations-util-mkEnableOption-magic" />
-      <programlisting language="bash">
-lib.mkEnableOption &quot;magic&quot;
-# is like
-lib.mkOption {
-  type = lib.types.bool;
-  default = false;
-  example = true;
-  description = lib.mdDoc &quot;Whether to enable magic.&quot;;
-}
-</programlisting>
-      <section xml:id="sec-option-declarations-util-mkPackageOption">
-        <title><literal>mkPackageOption</literal></title>
-        <para>
-          Usage:
-        </para>
-        <programlisting language="bash">
-mkPackageOption pkgs &quot;name&quot; { default = [ &quot;path&quot; &quot;in&quot; &quot;pkgs&quot; ]; example = &quot;literal example&quot;; }
-</programlisting>
-        <para>
-          Creates an Option attribute set for an option that specifies
-          the package a module should use for some purpose.
-        </para>
-        <para>
-          <emphasis role="strong">Note</emphasis>: You shouldn’t
-          necessarily make package options for all of your modules. You
-          can always overwrite a specific package throughout nixpkgs by
-          using
-          <link xlink:href="https://nixos.org/manual/nixpkgs/stable/#chap-overlays">nixpkgs
-          overlays</link>.
-        </para>
-        <para>
-          The default package is specified as a list of strings
-          representing its attribute path in nixpkgs. Because of this,
-          you need to pass nixpkgs itself as the first argument.
-        </para>
-        <para>
-          The second argument is the name of the option, used in the
-          description <quote>The &lt;name&gt; package to use.</quote>.
-          You can also pass an example value, either a literal string or
-          a package’s attribute path.
-        </para>
-        <para>
-          You can omit the default path if the name of the option is
-          also attribute path in nixpkgs.
-        </para>
-        <anchor xml:id="ex-options-declarations-util-mkPackageOption" />
-        <para>
-          Examples:
-        </para>
-        <anchor xml:id="ex-options-declarations-util-mkPackageOption-hello" />
-        <programlisting language="bash">
-lib.mkPackageOption pkgs &quot;hello&quot; { }
-# is like
-lib.mkOption {
-  type = lib.types.package;
-  default = pkgs.hello;
-  defaultText = lib.literalExpression &quot;pkgs.hello&quot;;
-  description = lib.mdDoc &quot;The hello package to use.&quot;;
-}
-</programlisting>
-        <anchor xml:id="ex-options-declarations-util-mkPackageOption-ghc" />
-        <programlisting language="bash">
-lib.mkPackageOption pkgs &quot;GHC&quot; {
-  default = [ &quot;ghc&quot; ];
-  example = &quot;pkgs.haskell.packages.ghc92.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.packages.ghc92.ghc.withPackages (hkgs: [ hkgs.primes ])&quot;;
-  description = lib.mdDoc &quot;The GHC package to use.&quot;;
-}
-</programlisting>
-        <section xml:id="sec-option-declarations-eot">
-          <title>Extensible Option Types</title>
-          <para>
-            Extensible option types is a feature that allow to extend
-            certain types declaration through multiple module files.
-            This feature only work with a restricted set of types,
-            namely <literal>enum</literal> and
-            <literal>submodules</literal> and any composed forms of
-            them.
-          </para>
-          <para>
-            Extensible option types can be used for
-            <literal>enum</literal> options that affects multiple
-            modules, or as an alternative to related
-            <literal>enable</literal> options.
-          </para>
-          <para>
-            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 ...).
-          </para>
-          <para>
-            There are two approaches we could take with this module
-            structure:
-          </para>
-          <itemizedlist>
-            <listitem>
-              <para>
-                Configuring the display managers independently by adding
-                an enable option to every display manager module
-                backend. (NixOS)
-              </para>
-            </listitem>
-            <listitem>
-              <para>
-                Configuring the display managers in the central module
-                by adding an option to select which display manager
-                backend to use.
-              </para>
-            </listitem>
-          </itemizedlist>
-          <para>
-            Both approaches have problems.
-          </para>
-          <para>
-            Making backends independent can quickly become hard to
-            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 explicitly by adding assertions
-            in each display manager backend module.
-          </para>
-          <para>
-            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>
-            By using extensible option types, it is possible to create a
-            placeholder option in the central module
-            (<link linkend="ex-option-declaration-eot-service">Example:
-            Extensible type placeholder in the service module</link>),
-            and to extend it in each backend module
-            (<link linkend="ex-option-declaration-eot-backend-gdm">Example:
-            Extending
-            <literal>services.xserver.displayManager.enable</literal> in
-            the <literal>gdm</literal> module</link>,
-            <link linkend="ex-option-declaration-eot-backend-sddm">Example:
-            Extending
-            <literal>services.xserver.displayManager.enable</literal> in
-            the <literal>sddm</literal> module</link>).
-          </para>
-          <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 enforces that there
-            can only be a single display manager enabled.
-          </para>
-          <anchor xml:id="ex-option-declaration-eot-service" />
-          <para>
-            <emphasis role="strong">Example: Extensible type placeholder
-            in the service module</emphasis>
-          </para>
-          <programlisting language="bash">
-services.xserver.displayManager.enable = mkOption {
-  description = &quot;Display manager to use&quot;;
-  type = with types; nullOr (enum [ ]);
-};
-</programlisting>
-          <anchor xml:id="ex-option-declaration-eot-backend-gdm" />
-          <para>
-            <emphasis role="strong">Example: Extending
-            <literal>services.xserver.displayManager.enable</literal> in
-            the <literal>gdm</literal> module</emphasis>
-          </para>
-          <programlisting language="bash">
-services.xserver.displayManager.enable = mkOption {
-  type = with types; nullOr (enum [ &quot;gdm&quot; ]);
-};
-</programlisting>
-          <anchor xml:id="ex-option-declaration-eot-backend-sddm" />
-          <para>
-            <emphasis role="strong">Example: Extending
-            <literal>services.xserver.displayManager.enable</literal> in
-            the <literal>sddm</literal> module</emphasis>
-          </para>
-          <programlisting language="bash">
-services.xserver.displayManager.enable = mkOption {
-  type = with types; nullOr (enum [ &quot;sddm&quot; ]);
-};
-</programlisting>
-          <para>
-            The placeholder declaration is a standard
-            <literal>mkOption</literal> declaration, but it is important
-            that extensible option declarations only use the
-            <literal>type</literal> argument.
-          </para>
-          <para>
-            Extensible option types work with any of the composed
-            variants of <literal>enum</literal> such as
-            <literal>with types; nullOr (enum [ &quot;foo&quot; &quot;bar&quot; ])</literal>
-            or
-            <literal>with types; listOf (enum [ &quot;foo&quot; &quot;bar&quot; ])</literal>.
-          </para>
-        </section>
-      </section>
-    </section>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/development/option-def.section.xml b/nixos/doc/manual/from_md/development/option-def.section.xml
deleted file mode 100644
index 3c1a979e70f..00000000000
--- a/nixos/doc/manual/from_md/development/option-def.section.xml
+++ /dev/null
@@ -1,132 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-option-definitions">
-  <title>Option Definitions</title>
-  <para>
-    Option definitions are generally straight-forward bindings of values
-    to option names, like
-  </para>
-  <programlisting language="bash">
-config = {
-  services.httpd.enable = true;
-};
-</programlisting>
-  <para>
-    However, sometimes you need to wrap an option definition or set of
-    option definitions in a <emphasis>property</emphasis> to achieve
-    certain effects:
-  </para>
-  <section xml:id="sec-option-definitions-delaying-conditionals">
-    <title>Delaying Conditionals</title>
-    <para>
-      If a set of option definitions is conditional on the value of
-      another option, you may need to use <literal>mkIf</literal>.
-      Consider, for instance:
-    </para>
-    <programlisting language="bash">
-config = if config.services.httpd.enable then {
-  environment.systemPackages = [ ... ];
-  ...
-} else {};
-</programlisting>
-    <para>
-      This definition will cause Nix to fail with an <quote>infinite
-      recursion</quote> error. Why? Because the value of
-      <literal>config.services.httpd.enable</literal> depends on the
-      value being constructed here. After all, you could also write the
-      clearly circular and contradictory:
-    </para>
-    <programlisting language="bash">
-config = if config.services.httpd.enable then {
-  services.httpd.enable = false;
-} else {
-  services.httpd.enable = true;
-};
-</programlisting>
-    <para>
-      The solution is to write:
-    </para>
-    <programlisting language="bash">
-config = mkIf config.services.httpd.enable {
-  environment.systemPackages = [ ... ];
-  ...
-};
-</programlisting>
-    <para>
-      The special function <literal>mkIf</literal> causes the evaluation
-      of the conditional to be <quote>pushed down</quote> into the
-      individual definitions, as if you had written:
-    </para>
-    <programlisting language="bash">
-config = {
-  environment.systemPackages = if config.services.httpd.enable then [ ... ] else [];
-  ...
-};
-</programlisting>
-  </section>
-  <section xml:id="sec-option-definitions-setting-priorities">
-    <title>Setting Priorities</title>
-    <para>
-      A module can override the definitions of an option in other
-      modules by setting an <emphasis>override priority</emphasis>. All
-      option definitions that do not have the lowest priority value are
-      discarded. By default, option definitions have priority 100 and
-      option defaults have priority 1500. You can specify an explicit
-      priority by using <literal>mkOverride</literal>, e.g.
-    </para>
-    <programlisting language="bash">
-services.openssh.enable = mkOverride 10 false;
-</programlisting>
-    <para>
-      This definition causes all other definitions with priorities above
-      10 to be discarded. The function <literal>mkForce</literal> is
-      equal to <literal>mkOverride 50</literal>, and
-      <literal>mkDefault</literal> is equal to
-      <literal>mkOverride 1000</literal>.
-    </para>
-  </section>
-  <section xml:id="sec-option-definitions-ordering">
-    <title>Ordering Definitions</title>
-    <para>
-      It is also possible to influence the order in which the
-      definitions for an option are merged by setting an <emphasis>order
-      priority</emphasis> with <literal>mkOrder</literal>. The default
-      order priority is 1000. The functions <literal>mkBefore</literal>
-      and <literal>mkAfter</literal> are equal to
-      <literal>mkOrder 500</literal> and
-      <literal>mkOrder 1500</literal>, respectively. As an example,
-    </para>
-    <programlisting language="bash">
-hardware.firmware = mkBefore [ myFirmware ];
-</programlisting>
-    <para>
-      This definition ensures that <literal>myFirmware</literal> comes
-      before other unordered definitions in the final list value of
-      <literal>hardware.firmware</literal>.
-    </para>
-    <para>
-      Note that this is different from
-      <link linkend="sec-option-definitions-setting-priorities">override
-      priorities</link>: setting an order does not affect whether the
-      definition is included or not.
-    </para>
-  </section>
-  <section xml:id="sec-option-definitions-merging">
-    <title>Merging Configurations</title>
-    <para>
-      In conjunction with <literal>mkIf</literal>, it is sometimes
-      useful for a module to return multiple sets of option definitions,
-      to be merged together as if they were declared in separate
-      modules. This can be done using <literal>mkMerge</literal>:
-    </para>
-    <programlisting language="bash">
-config = mkMerge
-  [ # Unconditional stuff.
-    { environment.systemPackages = [ ... ];
-    }
-    # Conditional stuff.
-    (mkIf config.services.bla.enable {
-      environment.systemPackages = [ ... ];
-    })
-  ];
-</programlisting>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/development/option-types.section.xml b/nixos/doc/manual/from_md/development/option-types.section.xml
deleted file mode 100644
index c0f40cb3423..00000000000
--- a/nixos/doc/manual/from_md/development/option-types.section.xml
+++ /dev/null
@@ -1,1148 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-option-types">
-  <title>Options Types</title>
-  <para>
-    Option types are a way to put constraints on the values a module
-    option can take. Types are also responsible of how values are merged
-    in case of multiple value definitions.
-  </para>
-  <section xml:id="sec-option-types-basic">
-    <title>Basic types</title>
-    <para>
-      Basic types are the simplest available types in the module system.
-      Basic types include multiple string types that mainly differ in
-      how definition merging is handled.
-    </para>
-    <variablelist>
-      <varlistentry>
-        <term>
-          <literal>types.bool</literal>
-        </term>
-        <listitem>
-          <para>
-            A boolean, its values can be <literal>true</literal> or
-            <literal>false</literal>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.path</literal>
-        </term>
-        <listitem>
-          <para>
-            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>
-      <varlistentry>
-        <term>
-          <literal>types.package</literal>
-        </term>
-        <listitem>
-          <para>
-            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>
-      <varlistentry>
-        <term>
-          <literal>types.enum</literal>
-          <emphasis><literal>l</literal></emphasis>
-        </term>
-        <listitem>
-          <para>
-            One element of the list
-            <emphasis><literal>l</literal></emphasis>, e.g.
-            <literal>types.enum [ &quot;left&quot; &quot;right&quot; ]</literal>.
-            Multiple definitions cannot be merged.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.anything</literal>
-        </term>
-        <listitem>
-          <para>
-            A type that accepts any value and recursively merges
-            attribute sets together. This type is recommended when the
-            option type is unknown.
-          </para>
-          <anchor xml:id="ex-types-anything" />
-          <para>
-            <emphasis role="strong">Example:
-            <literal>types.anything</literal> Example</emphasis>
-          </para>
-          <para>
-            Two definitions of this type like
-          </para>
-          <programlisting language="bash">
-{
-  str = lib.mkDefault &quot;foo&quot;;
-  pkg.hello = pkgs.hello;
-  fun.fun = x: x + 1;
-}
-</programlisting>
-          <programlisting language="bash">
-{
-  str = lib.mkIf true &quot;bar&quot;;
-  pkg.gcc = pkgs.gcc;
-  fun.fun = lib.mkForce (x: x + 2);
-}
-</programlisting>
-          <para>
-            will get merged to
-          </para>
-          <programlisting language="bash">
-{
-  str = &quot;bar&quot;;
-  pkg.gcc = pkgs.gcc;
-  pkg.hello = pkgs.hello;
-  fun.fun = x: x + 2;
-}
-</programlisting>
-        </listitem>
-      </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>
-          <para>
-            A free-form attribute set.
-          </para>
-          <warning>
-            <para>
-              This type will be deprecated in the future because it
-              doesn't recurse into attribute sets, silently drops
-              earlier attribute definitions, and doesn't discharge
-              <literal>lib.mkDefault</literal>,
-              <literal>lib.mkIf</literal> and co. For allowing arbitrary
-              attribute sets, prefer
-              <literal>types.attrsOf types.anything</literal> instead
-              which doesn't have these problems.
-            </para>
-          </warning>
-        </listitem>
-      </varlistentry>
-    </variablelist>
-    <section xml:id="sec-option-types-numeric">
-      <title>Numeric types</title>
-      <variablelist>
-        <varlistentry>
-          <term>
-            <literal>types.int</literal>
-          </term>
-          <listitem>
-            <para>
-              A signed integer.
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.ints.{s8, s16, s32}</literal>
-          </term>
-          <listitem>
-            <para>
-              Signed integers with a fixed length (8, 16 or 32 bits).
-              They go from −2^n/2 to 2^n/2−1 respectively (e.g.
-              <literal>−128</literal> to <literal>127</literal> for 8
-              bits).
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.ints.unsigned</literal>
-          </term>
-          <listitem>
-            <para>
-              An unsigned integer (that is &gt;= 0).
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.ints.{u8, u16, u32}</literal>
-          </term>
-          <listitem>
-            <para>
-              Unsigned integers with a fixed length (8, 16 or 32 bits).
-              They go from 0 to 2^n−1 respectively (e.g.
-              <literal>0</literal> to <literal>255</literal> for 8
-              bits).
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.ints.between</literal>
-            <emphasis><literal>lowest highest</literal></emphasis>
-          </term>
-          <listitem>
-            <para>
-              An integer between
-              <emphasis><literal>lowest</literal></emphasis> and
-              <emphasis><literal>highest</literal></emphasis> (both
-              inclusive).
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.ints.positive</literal>
-          </term>
-          <listitem>
-            <para>
-              A positive integer (that is &gt; 0).
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.port</literal>
-          </term>
-          <listitem>
-            <para>
-              A port number. This type is an alias to
-              <literal>types.ints.u16</literal>.
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.float</literal>
-          </term>
-          <listitem>
-            <para>
-              A floating point number.
-            </para>
-            <warning>
-              <para>
-                Converting a floating point number to a string with
-                <literal>toString</literal> or <literal>toJSON</literal>
-                may result in
-                <link xlink:href="https://github.com/NixOS/nix/issues/5733">precision
-                loss</link>.
-              </para>
-            </warning>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.number</literal>
-          </term>
-          <listitem>
-            <para>
-              Either a signed integer or a floating point number. No
-              implicit conversion is done between the two types, and
-              multiple equal definitions will only be merged if they
-              have the same type.
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.numbers.between</literal>
-            <emphasis><literal>lowest highest</literal></emphasis>
-          </term>
-          <listitem>
-            <para>
-              An integer or floating point number between
-              <emphasis><literal>lowest</literal></emphasis> and
-              <emphasis><literal>highest</literal></emphasis> (both
-              inclusive).
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.numbers.nonnegative</literal>
-          </term>
-          <listitem>
-            <para>
-              A nonnegative integer or floating point number (that is
-              &gt;= 0).
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.numbers.positive</literal>
-          </term>
-          <listitem>
-            <para>
-              A positive integer or floating point number (that is &gt;
-              0).
-            </para>
-          </listitem>
-        </varlistentry>
-      </variablelist>
-    </section>
-    <section xml:id="sec-option-types-string">
-      <title>String types</title>
-      <variablelist>
-        <varlistentry>
-          <term>
-            <literal>types.str</literal>
-          </term>
-          <listitem>
-            <para>
-              A string. Multiple definitions cannot be merged.
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.separatedString</literal>
-            <emphasis><literal>sep</literal></emphasis>
-          </term>
-          <listitem>
-            <para>
-              A string. Multiple definitions are concatenated with
-              <emphasis><literal>sep</literal></emphasis>, e.g.
-              <literal>types.separatedString &quot;|&quot;</literal>.
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.lines</literal>
-          </term>
-          <listitem>
-            <para>
-              A string. Multiple definitions are concatenated with a new
-              line <literal>&quot;\n&quot;</literal>.
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.commas</literal>
-          </term>
-          <listitem>
-            <para>
-              A string. Multiple definitions are concatenated with a
-              comma <literal>&quot;,&quot;</literal>.
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.envVar</literal>
-          </term>
-          <listitem>
-            <para>
-              A string. Multiple definitions are concatenated with a
-              colon <literal>&quot;:&quot;</literal>.
-            </para>
-          </listitem>
-        </varlistentry>
-        <varlistentry>
-          <term>
-            <literal>types.strMatching</literal>
-          </term>
-          <listitem>
-            <para>
-              A string matching a specific regular expression. Multiple
-              definitions cannot be merged. The regular expression is
-              processed using <literal>builtins.match</literal>.
-            </para>
-          </listitem>
-        </varlistentry>
-      </variablelist>
-    </section>
-  </section>
-  <section xml:id="sec-option-types-submodule">
-    <title>Submodule types</title>
-    <para>
-      Submodules are detailed in
-      <link linkend="section-option-types-submodule">Submodule</link>.
-    </para>
-    <variablelist>
-      <varlistentry>
-        <term>
-          <literal>types.submodule</literal>
-          <emphasis><literal>o</literal></emphasis>
-        </term>
-        <listitem>
-          <para>
-            A set of sub options
-            <emphasis><literal>o</literal></emphasis>.
-            <emphasis><literal>o</literal></emphasis> can be an
-            attribute set, a function returning an attribute set, or a
-            path to a file containing such a value. Submodules are used
-            in composed types to create modular options. This is
-            equivalent to
-            <literal>types.submoduleWith { modules = toList o; shorthandOnlyDefinesConfig = true; }</literal>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.submoduleWith</literal> {
-          <emphasis><literal>modules</literal></emphasis>,
-          <emphasis><literal>specialArgs</literal></emphasis> ? {},
-          <emphasis><literal>shorthandOnlyDefinesConfig</literal></emphasis>
-          ? false }
-        </term>
-        <listitem>
-          <para>
-            Like <literal>types.submodule</literal>, but more flexible
-            and with better defaults. It has parameters
-          </para>
-          <itemizedlist>
-            <listitem>
-              <para>
-                <emphasis><literal>modules</literal></emphasis> A list
-                of modules to use by default for this submodule type.
-                This gets combined with all option definitions to build
-                the final list of modules that will be included.
-              </para>
-              <note>
-                <para>
-                  Only options defined with this argument are included
-                  in rendered documentation.
-                </para>
-              </note>
-            </listitem>
-            <listitem>
-              <para>
-                <emphasis><literal>specialArgs</literal></emphasis> An
-                attribute set of extra arguments to be passed to the
-                module functions. The option
-                <literal>_module.args</literal> should be used instead
-                for most arguments since it allows overriding.
-                <emphasis><literal>specialArgs</literal></emphasis>
-                should only be used for arguments that can't go through
-                the module fixed-point, because of infinite recursion or
-                other problems. An example is overriding the
-                <literal>lib</literal> argument, because
-                <literal>lib</literal> itself is used to define
-                <literal>_module.args</literal>, which makes using
-                <literal>_module.args</literal> to define it impossible.
-              </para>
-            </listitem>
-            <listitem>
-              <para>
-                <emphasis><literal>shorthandOnlyDefinesConfig</literal></emphasis>
-                Whether definitions of this type should default to the
-                <literal>config</literal> section of a module (see
-                <link linkend="ex-module-syntax">Example: Structure of
-                NixOS Modules</link>) if it is an attribute set.
-                Enabling this only has a benefit when the submodule
-                defines an option named <literal>config</literal> or
-                <literal>options</literal>. In such a case it would
-                allow the option to be set with
-                <literal>the-submodule.config = &quot;value&quot;</literal>
-                instead of requiring
-                <literal>the-submodule.config.config = &quot;value&quot;</literal>.
-                This is because only when modules
-                <emphasis>don't</emphasis> set the
-                <literal>config</literal> or <literal>options</literal>
-                keys, all keys are interpreted as option definitions in
-                the <literal>config</literal> section. Enabling this
-                option implicitly puts all attributes in the
-                <literal>config</literal> section.
-              </para>
-              <para>
-                With this option enabled, defining a
-                non-<literal>config</literal> section requires using a
-                function:
-                <literal>the-submodule = { ... }: { options = { ... }; }</literal>.
-              </para>
-            </listitem>
-          </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">
-    <title>Composed types</title>
-    <para>
-      Composed types are types that take a type as parameter.
-      <literal>listOf int</literal> and
-      <literal>either int str</literal> are examples of composed types.
-    </para>
-    <variablelist>
-      <varlistentry>
-        <term>
-          <literal>types.listOf</literal>
-          <emphasis><literal>t</literal></emphasis>
-        </term>
-        <listitem>
-          <para>
-            A list of <emphasis><literal>t</literal></emphasis> type,
-            e.g. <literal>types.listOf int</literal>. Multiple
-            definitions are merged with list concatenation.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.attrsOf</literal>
-          <emphasis><literal>t</literal></emphasis>
-        </term>
-        <listitem>
-          <para>
-            An attribute set of where all the values are of
-            <emphasis><literal>t</literal></emphasis> type. Multiple
-            definitions result in the joined attribute set.
-          </para>
-          <note>
-            <para>
-              This type is <emphasis>strict</emphasis> in its values,
-              which in turn means attributes cannot depend on other
-              attributes. See <literal> types.lazyAttrsOf</literal> for
-              a lazy version.
-            </para>
-          </note>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.lazyAttrsOf</literal>
-          <emphasis><literal>t</literal></emphasis>
-        </term>
-        <listitem>
-          <para>
-            An attribute set of where all the values are of
-            <emphasis><literal>t</literal></emphasis> type. Multiple
-            definitions result in the joined attribute set. This is the
-            lazy version of <literal>types.attrsOf </literal>, allowing
-            attributes to depend on each other.
-          </para>
-          <warning>
-            <para>
-              This version does not fully support conditional
-              definitions! With an option <literal>foo</literal> of this
-              type and a definition
-              <literal>foo.attr = lib.mkIf false 10</literal>,
-              evaluating <literal>foo ? attr</literal> will return
-              <literal>true</literal> even though it should be false.
-              Accessing the value will then throw an error. For types
-              <emphasis><literal>t</literal></emphasis> that have an
-              <literal>emptyValue</literal> defined, that value will be
-              returned instead of throwing an error. So if the type of
-              <literal>foo.attr</literal> was
-              <literal>lazyAttrsOf (nullOr int)</literal>,
-              <literal>null</literal> would be returned instead for the
-              same <literal>mkIf false</literal> definition.
-            </para>
-          </warning>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.nullOr</literal>
-          <emphasis><literal>t</literal></emphasis>
-        </term>
-        <listitem>
-          <para>
-            <literal>null</literal> or type
-            <emphasis><literal>t</literal></emphasis>. Multiple
-            definitions are merged according to type
-            <emphasis><literal>t</literal></emphasis>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.uniq</literal>
-          <emphasis><literal>t</literal></emphasis>
-        </term>
-        <listitem>
-          <para>
-            Ensures that type <emphasis><literal>t</literal></emphasis>
-            cannot be merged. It is used to ensure option definitions
-            are declared only once.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.unique</literal>
-          <literal>{ message = m }</literal>
-          <emphasis><literal>t</literal></emphasis>
-        </term>
-        <listitem>
-          <para>
-            Ensures that type <emphasis><literal>t</literal></emphasis>
-            cannot be merged. Prints the message
-            <emphasis><literal>m</literal></emphasis>, after the line
-            <literal>The option &lt;option path&gt; is defined multiple times.</literal>
-            and before a list of definition locations.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.either</literal>
-          <emphasis><literal>t1 t2</literal></emphasis>
-        </term>
-        <listitem>
-          <para>
-            Type <emphasis><literal>t1</literal></emphasis> or type
-            <emphasis><literal>t2</literal></emphasis>, e.g.
-            <literal>with types; either int str</literal>. Multiple
-            definitions cannot be merged.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.oneOf</literal> [
-          <emphasis><literal>t1 t2</literal></emphasis> ... ]
-        </term>
-        <listitem>
-          <para>
-            Type <emphasis><literal>t1</literal></emphasis> or type
-            <emphasis><literal>t2</literal></emphasis> and so forth,
-            e.g. <literal>with types; oneOf [ int str bool ]</literal>.
-            Multiple definitions cannot be merged.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>types.coercedTo</literal>
-          <emphasis><literal>from f to</literal></emphasis>
-        </term>
-        <listitem>
-          <para>
-            Type <emphasis><literal>to</literal></emphasis> or type
-            <emphasis><literal>from</literal></emphasis> which will be
-            coerced to type <emphasis><literal>to</literal></emphasis>
-            using function <emphasis><literal>f</literal></emphasis>
-            which takes an argument of type
-            <emphasis><literal>from</literal></emphasis> and return a
-            value of type <emphasis><literal>to</literal></emphasis>.
-            Can be used to preserve backwards compatibility of an option
-            if its type was changed.
-          </para>
-        </listitem>
-      </varlistentry>
-    </variablelist>
-  </section>
-  <section xml:id="section-option-types-submodule">
-    <title>Submodule</title>
-    <para>
-      <literal>submodule</literal> is a very powerful type that defines
-      a set of sub-options that are handled like a separate module.
-    </para>
-    <para>
-      It takes a parameter <emphasis><literal>o</literal></emphasis>,
-      that should be a set, or a function returning a set with an
-      <literal>options</literal> key defining the sub-options. Submodule
-      option definitions are type-checked accordingly to the
-      <literal>options</literal> declarations. Of course, you can nest
-      submodule option definitions for even higher modularity.
-    </para>
-    <para>
-      The option set can be defined directly
-      (<link linkend="ex-submodule-direct">Example: Directly defined
-      submodule</link>) or as reference
-      (<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
-      submodule</emphasis>
-    </para>
-    <programlisting language="bash">
-options.mod = mkOption {
-  description = &quot;submodule example&quot;;
-  type = with types; submodule {
-    options = {
-      foo = mkOption {
-        type = int;
-      };
-      bar = mkOption {
-        type = str;
-      };
-    };
-  };
-};
-</programlisting>
-    <anchor xml:id="ex-submodule-reference" />
-    <para>
-      <emphasis role="strong">Example: Submodule defined as a
-      reference</emphasis>
-    </para>
-    <programlisting language="bash">
-let
-  modOptions = {
-    options = {
-      foo = mkOption {
-        type = int;
-      };
-      bar = mkOption {
-        type = int;
-      };
-    };
-  };
-in
-options.mod = mkOption {
-  description = &quot;submodule example&quot;;
-  type = with types; submodule modOptions;
-};
-</programlisting>
-    <para>
-      The <literal>submodule</literal> type is especially interesting
-      when used with composed types like <literal>attrsOf</literal> or
-      <literal>listOf</literal>. When composed with
-      <literal>listOf</literal>
-      (<link linkend="ex-submodule-listof-declaration">Example:
-      Declaration of a list of submodules</link>),
-      <literal>submodule</literal> allows multiple definitions of the
-      submodule option set
-      (<link linkend="ex-submodule-listof-definition">Example:
-      Definition of a list of submodules</link>).
-    </para>
-    <anchor xml:id="ex-submodule-listof-declaration" />
-    <para>
-      <emphasis role="strong">Example: Declaration of a list of
-      submodules</emphasis>
-    </para>
-    <programlisting language="bash">
-options.mod = mkOption {
-  description = &quot;submodule example&quot;;
-  type = with types; listOf (submodule {
-    options = {
-      foo = mkOption {
-        type = int;
-      };
-      bar = mkOption {
-        type = str;
-      };
-    };
-  });
-};
-</programlisting>
-    <anchor xml:id="ex-submodule-listof-definition" />
-    <para>
-      <emphasis role="strong">Example: Definition of a list of
-      submodules</emphasis>
-    </para>
-    <programlisting language="bash">
-config.mod = [
-  { foo = 1; bar = &quot;one&quot;; }
-  { foo = 2; bar = &quot;two&quot;; }
-];
-</programlisting>
-    <para>
-      When composed with <literal>attrsOf</literal>
-      (<link linkend="ex-submodule-attrsof-declaration">Example:
-      Declaration of attribute sets of submodules</link>),
-      <literal>submodule</literal> allows multiple named definitions of
-      the submodule option set
-      (<link linkend="ex-submodule-attrsof-definition">Example:
-      Definition of attribute sets of submodules</link>).
-    </para>
-    <anchor xml:id="ex-submodule-attrsof-declaration" />
-    <para>
-      <emphasis role="strong">Example: Declaration of attribute sets of
-      submodules</emphasis>
-    </para>
-    <programlisting language="bash">
-options.mod = mkOption {
-  description = &quot;submodule example&quot;;
-  type = with types; attrsOf (submodule {
-    options = {
-      foo = mkOption {
-        type = int;
-      };
-      bar = mkOption {
-        type = str;
-      };
-    };
-  });
-};
-</programlisting>
-    <anchor xml:id="ex-submodule-attrsof-definition" />
-    <para>
-      <emphasis role="strong">Example: Definition of attribute sets of
-      submodules</emphasis>
-    </para>
-    <programlisting language="bash">
-config.mod.one = { foo = 1; bar = &quot;one&quot;; };
-config.mod.two = { foo = 2; bar = &quot;two&quot;; };
-</programlisting>
-  </section>
-  <section xml:id="sec-option-types-extending">
-    <title>Extending types</title>
-    <para>
-      Types are mainly characterized by their <literal>check</literal>
-      and <literal>merge</literal> functions.
-    </para>
-    <variablelist>
-      <varlistentry>
-        <term>
-          <literal>check</literal>
-        </term>
-        <listitem>
-          <para>
-            The function to type check the value. Takes a value as
-            parameter and return a boolean. It is possible to extend a
-            type check with the <literal>addCheck</literal> function
-            (<link linkend="ex-extending-type-check-1">Example: Adding a
-            type check</link>), or to fully override the check function
-            (<link linkend="ex-extending-type-check-2">Example:
-            Overriding a type check</link>).
-          </para>
-          <anchor xml:id="ex-extending-type-check-1" />
-          <para>
-            <emphasis role="strong">Example: Adding a type
-            check</emphasis>
-          </para>
-          <programlisting language="bash">
-byte = mkOption {
-  description = &quot;An integer between 0 and 255.&quot;;
-  type = types.addCheck types.int (x: x &gt;= 0 &amp;&amp; x &lt;= 255);
-};
-</programlisting>
-          <anchor xml:id="ex-extending-type-check-2" />
-          <para>
-            <emphasis role="strong">Example: Overriding a type
-            check</emphasis>
-          </para>
-          <programlisting language="bash">
-nixThings = mkOption {
-  description = &quot;words that start with 'nix'&quot;;
-  type = types.str // {
-    check = (x: lib.hasPrefix &quot;nix&quot; x)
-  };
-};
-</programlisting>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>merge</literal>
-        </term>
-        <listitem>
-          <para>
-            Function to merge the options values when multiple values
-            are set. The function takes two parameters,
-            <literal>loc</literal> the option path as a list of strings,
-            and <literal>defs</literal> the list of defined values as a
-            list. It is possible to override a type merge function for
-            custom needs.
-          </para>
-        </listitem>
-      </varlistentry>
-    </variablelist>
-  </section>
-  <section xml:id="sec-option-types-custom">
-    <title>Custom types</title>
-    <para>
-      Custom types can be created with the
-      <literal>mkOptionType</literal> function. As type creation
-      includes some more complex topics such as submodule handling, it
-      is recommended to get familiar with <literal>types.nix</literal>
-      code before creating a new type.
-    </para>
-    <para>
-      The only required parameter is <literal>name</literal>.
-    </para>
-    <variablelist>
-      <varlistentry>
-        <term>
-          <literal>name</literal>
-        </term>
-        <listitem>
-          <para>
-            A string representation of the type function name.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>definition</literal>
-        </term>
-        <listitem>
-          <para>
-            Description of the type used in documentation. Give
-            information of the type and any of its arguments.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>check</literal>
-        </term>
-        <listitem>
-          <para>
-            A function to type check the definition value. Takes the
-            definition value as a parameter and returns a boolean
-            indicating the type check result, <literal>true</literal>
-            for success and <literal>false</literal> for failure.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>merge</literal>
-        </term>
-        <listitem>
-          <para>
-            A function to merge multiple definitions values. Takes two
-            parameters:
-          </para>
-          <variablelist>
-            <varlistentry>
-              <term>
-                <emphasis><literal>loc</literal></emphasis>
-              </term>
-              <listitem>
-                <para>
-                  The option path as a list of strings, e.g.
-                  <literal>[&quot;boot&quot; &quot;loader &quot;grub&quot; &quot;enable&quot;]</literal>.
-                </para>
-              </listitem>
-            </varlistentry>
-            <varlistentry>
-              <term>
-                <emphasis><literal>defs</literal></emphasis>
-              </term>
-              <listitem>
-                <para>
-                  The list of sets of defined <literal>value</literal>
-                  and <literal>file</literal> where the value was
-                  defined, e.g.
-                  <literal>[ { file = &quot;/foo.nix&quot;; value = 1; } { file = &quot;/bar.nix&quot;; value = 2 } ]</literal>.
-                  The <literal>merge</literal> function should return
-                  the merged value or throw an error in case the values
-                  are impossible or not meant to be merged.
-                </para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>getSubOptions</literal>
-        </term>
-        <listitem>
-          <para>
-            For composed types that can take a submodule as type
-            parameter, this function generate sub-options documentation.
-            It takes the current option prefix as a list and return the
-            set of sub-options. Usually defined in a recursive manner by
-            adding a term to the prefix, e.g.
-            <literal>prefix: elemType.getSubOptions (prefix ++ [&quot;prefix&quot;])</literal>
-            where
-            <emphasis><literal>&quot;prefix&quot;</literal></emphasis>
-            is the newly added prefix.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>getSubModules</literal>
-        </term>
-        <listitem>
-          <para>
-            For composed types that can take a submodule as type
-            parameter, this function should return the type parameters
-            submodules. If the type parameter is called
-            <literal>elemType</literal>, the function should just
-            recursively look into submodules by returning
-            <literal>elemType.getSubModules;</literal>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>substSubModules</literal>
-        </term>
-        <listitem>
-          <para>
-            For composed types that can take a submodule as type
-            parameter, this function can be used to substitute the
-            parameter of a submodule type. It takes a module as
-            parameter and return the type with the submodule options
-            substituted. It is usually defined as a type function call
-            with a recursive call to <literal>substSubModules</literal>,
-            e.g for a type <literal>composedType</literal> that take an
-            <literal>elemtype</literal> type parameter, this function
-            should be defined as
-            <literal>m: composedType (elemType.substSubModules m)</literal>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>typeMerge</literal>
-        </term>
-        <listitem>
-          <para>
-            A function to merge multiple type declarations. Takes the
-            type to merge <literal>functor</literal> as parameter. A
-            <literal>null</literal> return value means that type cannot
-            be merged.
-          </para>
-          <variablelist>
-            <varlistentry>
-              <term>
-                <emphasis><literal>f</literal></emphasis>
-              </term>
-              <listitem>
-                <para>
-                  The type to merge <literal>functor</literal>.
-                </para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-          <para>
-            Note: There is a generic <literal>defaultTypeMerge</literal>
-            that work with most of value and composed types.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>functor</literal>
-        </term>
-        <listitem>
-          <para>
-            An attribute set representing the type. It is used for type
-            operations and has the following keys:
-          </para>
-          <variablelist>
-            <varlistentry>
-              <term>
-                <literal>type</literal>
-              </term>
-              <listitem>
-                <para>
-                  The type function.
-                </para>
-              </listitem>
-            </varlistentry>
-            <varlistentry>
-              <term>
-                <literal>wrapped</literal>
-              </term>
-              <listitem>
-                <para>
-                  Holds the type parameter for composed types.
-                </para>
-              </listitem>
-            </varlistentry>
-            <varlistentry>
-              <term>
-                <literal>payload</literal>
-              </term>
-              <listitem>
-                <para>
-                  Holds the value parameter for value types. The types
-                  that have a <literal>payload</literal> are the
-                  <literal>enum</literal>,
-                  <literal>separatedString</literal> and
-                  <literal>submodule</literal> types.
-                </para>
-              </listitem>
-            </varlistentry>
-            <varlistentry>
-              <term>
-                <literal>binOp</literal>
-              </term>
-              <listitem>
-                <para>
-                  A binary operation that can merge the payloads of two
-                  same types. Defined as a function that take two
-                  payloads as parameters and return the payloads merged.
-                </para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-        </listitem>
-      </varlistentry>
-    </variablelist>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/development/replace-modules.section.xml b/nixos/doc/manual/from_md/development/replace-modules.section.xml
deleted file mode 100644
index cf8a39ba844..00000000000
--- a/nixos/doc/manual/from_md/development/replace-modules.section.xml
+++ /dev/null
@@ -1,70 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-replace-modules">
-  <title>Replace Modules</title>
-  <para>
-    Modules that are imported can also be disabled. The option
-    declarations, config implementation and the imports of a disabled
-    module will be ignored, allowing another to take it's place. This
-    can be used to import a set of modules from another channel while
-    keeping the rest of the system on a stable release.
-  </para>
-  <para>
-    <literal>disabledModules</literal> is a top level attribute like
-    <literal>imports</literal>, <literal>options</literal> and
-    <literal>config</literal>. It contains a list of modules that will
-    be disabled. This can either be the full path to the module or a
-    string with the filename relative to the modules path (eg.
-    &lt;nixpkgs/nixos/modules&gt; for nixos).
-  </para>
-  <para>
-    This example will replace the existing postgresql module with the
-    version defined in the nixos-unstable channel while keeping the rest
-    of the modules and packages from the original nixos channel. This
-    only overrides the module definition, this won't use postgresql from
-    nixos-unstable unless explicitly configured to do so.
-  </para>
-  <programlisting language="bash">
-{ config, lib, pkgs, ... }:
-
-{
-  disabledModules = [ &quot;services/databases/postgresql.nix&quot; ];
-
-  imports =
-    [ # Use postgresql service from nixos-unstable channel.
-      # sudo nix-channel --add https://nixos.org/channels/nixos-unstable nixos-unstable
-      &lt;nixos-unstable/nixos/modules/services/databases/postgresql.nix&gt;
-    ];
-
-  services.postgresql.enable = true;
-}
-</programlisting>
-  <para>
-    This example shows how to define a custom module as a replacement
-    for an existing module. Importing this module will disable the
-    original module without having to know it's implementation details.
-  </para>
-  <programlisting language="bash">
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.programs.man;
-in
-
-{
-  disabledModules = [ &quot;services/programs/man.nix&quot; ];
-
-  options = {
-    programs.man.enable = mkOption {
-      type = types.bool;
-      default = true;
-      description = &quot;Whether to enable manual pages.&quot;;
-    };
-  };
-
-  config = mkIf cfg.enabled {
-    warnings = [ &quot;disabled manpages for production deployments.&quot; ];
-  };
-}
-</programlisting>
-</section>
diff --git a/nixos/doc/manual/from_md/development/running-nixos-tests-interactively.section.xml b/nixos/doc/manual/from_md/development/running-nixos-tests-interactively.section.xml
deleted file mode 100644
index 16db709f8b9..00000000000
--- a/nixos/doc/manual/from_md/development/running-nixos-tests-interactively.section.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-running-nixos-tests-interactively">
-  <title>Running Tests interactively</title>
-  <para>
-    The test itself can be run interactively. This is particularly
-    useful when developing or debugging a test:
-  </para>
-  <programlisting>
-$ nix-build . -A nixosTests.login.driverInteractive
-$ ./result/bin/nixos-test-driver
-[...]
-&gt;&gt;&gt;
-</programlisting>
-  <para>
-    You can then take any Python statement, e.g.
-  </para>
-  <programlisting language="python">
-&gt;&gt;&gt; start_all()
-&gt;&gt;&gt; test_script()
-&gt;&gt;&gt; machine.succeed(&quot;touch /tmp/foo&quot;)
-&gt;&gt;&gt; print(machine.succeed(&quot;pwd&quot;)) # Show stdout of command
-</programlisting>
-  <para>
-    The function <literal>test_script</literal> executes the entire test
-    script and drops you back into the test driver command line upon its
-    completion. This allows you to inspect the state of the VMs after
-    the test (e.g. to debug the test script).
-  </para>
-  <section xml:id="sec-nixos-test-reuse-vm-state">
-    <title>Reuse VM state</title>
-    <para>
-      You can re-use the VM states coming from a previous run by setting
-      the <literal>--keep-vm-state</literal> flag.
-    </para>
-    <programlisting>
-$ ./result/bin/nixos-test-driver --keep-vm-state
-</programlisting>
-    <para>
-      The machine state is stored in the
-      <literal>$TMPDIR/vm-state-machinename</literal> directory.
-    </para>
-  </section>
-  <section xml:id="sec-nixos-test-interactive-configuration">
-    <title>Interactive-only test configuration</title>
-    <para>
-      The <literal>.driverInteractive</literal> attribute combines the
-      regular test configuration with definitions from the
-      <link linkend="test-opt-interactive"><literal>interactive</literal>
-      submodule</link>. This gives you a more usable, graphical, but
-      slightly different configuration.
-    </para>
-    <para>
-      You can add your own interactive-only test configuration by adding
-      extra configuration to the
-      <link linkend="test-opt-interactive"><literal>interactive</literal>
-      submodule</link>.
-    </para>
-    <para>
-      To interactively run only the regular configuration, build the
-      <literal>&lt;test&gt;.driver</literal> attribute instead, and call
-      it with the flag
-      <literal>result/bin/nixos-test-driver --interactive</literal>.
-    </para>
-  </section>
-</section>
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
deleted file mode 100644
index 23abb546899..00000000000
--- a/nixos/doc/manual/from_md/development/running-nixos-tests.section.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-running-nixos-tests">
-  <title>Running Tests</title>
-  <para>
-    You can run tests using <literal>nix-build</literal>. For example,
-    to run the test
-    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/login.nix"><literal>login.nix</literal></link>,
-    you do:
-  </para>
-  <programlisting>
-$ cd /my/git/clone/of/nixpkgs
-$ nix-build -A nixosTests.login
-</programlisting>
-  <para>
-    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 log of the test:
-  </para>
-  <programlisting>
-$ 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
deleted file mode 100644
index d26dd96243d..00000000000
--- a/nixos/doc/manual/from_md/development/settings-options.section.xml
+++ /dev/null
@@ -1,421 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-settings-options">
-  <title>Options for Program Settings</title>
-  <para>
-    Many programs have configuration files where program-specific
-    settings can be declared. File formats can be separated into two
-    categories:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        Nix-representable ones: These can trivially be mapped to a
-        subset of Nix syntax. E.g. JSON is an example, since its values
-        like <literal>{&quot;foo&quot;:{&quot;bar&quot;:10}}</literal>
-        can be mapped directly to Nix:
-        <literal>{ foo = { bar = 10; }; }</literal>. Other examples are
-        INI, YAML and TOML. The following section explains the
-        convention for these settings.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Non-nix-representable ones: These can't be trivially mapped to a
-        subset of Nix syntax. Most generic programming languages are in
-        this group, e.g. bash, since the statement
-        <literal>if true; then echo hi; fi</literal> doesn't have a
-        trivial representation in Nix.
-      </para>
-      <para>
-        Currently there are no fixed conventions for these, but it is
-        common to have a <literal>configFile</literal> option for
-        setting the configuration file path directly. The default value
-        of <literal>configFile</literal> can be an auto-generated file,
-        with convenient options for controlling the contents. For
-        example an option of type <literal>attrsOf str</literal> can be
-        used for representing environment variables which generates a
-        section like <literal>export FOO=&quot;foo&quot;</literal>.
-        Often it can also be useful to also include an
-        <literal>extraConfig</literal> option of type
-        <literal>lines</literal> to allow arbitrary text after the
-        autogenerated part of the file.
-      </para>
-    </listitem>
-  </itemizedlist>
-  <section xml:id="sec-settings-nix-representable">
-    <title>Nix-representable Formats (JSON, YAML, TOML, INI,
-    ...)</title>
-    <para>
-      By convention, formats like this are handled with a generic
-      <literal>settings</literal> option, representing the full program
-      configuration as a Nix value. The type of this option should
-      represent the format. The most common formats have a predefined
-      type and string generator already declared under
-      <literal>pkgs.formats</literal>:
-    </para>
-    <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>
-          <para>
-            A function taking an empty attribute set (for future
-            extensibility) and returning a set with JSON-specific
-            attributes <literal>type</literal> and
-            <literal>generate</literal> as specified
-            <link linkend="pkgs-formats-result">below</link>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>pkgs.formats.yaml</literal> { }
-        </term>
-        <listitem>
-          <para>
-            A function taking an empty attribute set (for future
-            extensibility) and returning a set with YAML-specific
-            attributes <literal>type</literal> and
-            <literal>generate</literal> as specified
-            <link linkend="pkgs-formats-result">below</link>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>pkgs.formats.ini</literal> {
-          <emphasis><literal>listsAsDuplicateKeys</literal></emphasis> ?
-          false, <emphasis><literal>listToValue</literal></emphasis> ?
-          null, ... }
-        </term>
-        <listitem>
-          <para>
-            A function taking an attribute set with values
-          </para>
-          <variablelist>
-            <varlistentry>
-              <term>
-                <literal>listsAsDuplicateKeys</literal>
-              </term>
-              <listitem>
-                <para>
-                  A boolean for controlling whether list values can be
-                  used to represent duplicate INI keys
-                </para>
-              </listitem>
-            </varlistentry>
-            <varlistentry>
-              <term>
-                <literal>listToValue</literal>
-              </term>
-              <listitem>
-                <para>
-                  A function for turning a list of values into a single
-                  value.
-                </para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-          <para>
-            It returns a set with INI-specific attributes
-            <literal>type</literal> and <literal>generate</literal> as
-            specified <link linkend="pkgs-formats-result">below</link>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>pkgs.formats.toml</literal> { }
-        </term>
-        <listitem>
-          <para>
-            A function taking an empty attribute set (for future
-            extensibility) and returning a set with TOML-specific
-            attributes <literal>type</literal> and
-            <literal>generate</literal> as specified
-            <link linkend="pkgs-formats-result">below</link>.
-          </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:
-    </para>
-    <variablelist>
-      <varlistentry>
-        <term>
-          <literal>type</literal>
-        </term>
-        <listitem>
-          <para>
-            A module system type representing a value of the format
-          </para>
-        </listitem>
-      </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>
-        <listitem>
-          <para>
-            A function that can render a value of the format to a file.
-            Returns a file path.
-          </para>
-          <note>
-            <para>
-              This function puts the value contents in the Nix store. So
-              this should be avoided for secrets.
-            </para>
-          </note>
-        </listitem>
-      </varlistentry>
-    </variablelist>
-    <anchor xml:id="ex-settings-nix-representable" />
-    <para>
-      <emphasis role="strong">Example: Module with conventional
-      <literal>settings</literal> option</emphasis>
-    </para>
-    <para>
-      The following shows a module for an example program that uses a
-      JSON configuration file. It demonstrates how above values can be
-      used, along with some other related best practices. See the
-      comments for explanations.
-    </para>
-    <programlisting language="bash">
-{ options, config, lib, pkgs, ... }:
-let
-  cfg = config.services.foo;
-  # Define the settings format used for this program
-  settingsFormat = pkgs.formats.json {};
-in {
-
-  options.services.foo = {
-    enable = lib.mkEnableOption &quot;foo service&quot;;
-
-    settings = lib.mkOption {
-      # Setting this type allows for correct merging behavior
-      type = settingsFormat.type;
-      default = {};
-      description = ''
-        Configuration for foo, see
-        &lt;link xlink:href=&quot;https://example.com/docs/foo&quot;/&gt;
-        for supported settings.
-      '';
-    };
-  };
-
-  config = lib.mkIf cfg.enable {
-    # We can assign some default settings here to make the service work by just
-    # enabling it. We use `mkDefault` for values that can be changed without
-    # problems
-    services.foo.settings = {
-      # Fails at runtime without any value set
-      log_level = lib.mkDefault &quot;WARN&quot;;
-
-      # We assume systemd's `StateDirectory` is used, so we require this value,
-      # therefore no mkDefault
-      data_path = &quot;/var/lib/foo&quot;;
-
-      # Since we use this to create a user we need to know the default value at
-      # eval time
-      user = lib.mkDefault &quot;foo&quot;;
-    };
-
-    environment.etc.&quot;foo.json&quot;.source =
-      # The formats generator function takes a filename and the Nix value
-      # representing the format value and produces a filepath with that value
-      # rendered in the format
-      settingsFormat.generate &quot;foo-config.json&quot; cfg.settings;
-
-    # We know that the `user` attribute exists because we set a default value
-    # for it above, allowing us to use it without worries here
-    users.users.${cfg.settings.user} = { isSystemUser = true; };
-
-    # ...
-  };
-}
-</programlisting>
-    <section xml:id="sec-settings-attrs-options">
-      <title>Option declarations for attributes</title>
-      <para>
-        Some <literal>settings</literal> attributes may deserve some
-        extra care. They may need a different type, default or merging
-        behavior, or they are essential options that should show their
-        documentation in the manual. This can be done using
-        <xref linkend="sec-freeform-modules" />.
-      </para>
-      <para>
-        We extend above example using freeform modules to declare an
-        option for the port, which will enforce it to be a valid integer
-        and make it show up in the manual.
-      </para>
-      <anchor xml:id="ex-settings-typed-attrs" />
-      <para>
-        <emphasis role="strong">Example: Declaring a type-checked
-        <literal>settings</literal> attribute</emphasis>
-      </para>
-      <programlisting language="bash">
-settings = lib.mkOption {
-  type = lib.types.submodule {
-
-    freeformType = settingsFormat.type;
-
-    # Declare an option for the port such that the type is checked and this option
-    # is shown in the manual.
-    options.port = lib.mkOption {
-      type = lib.types.port;
-      default = 8080;
-      description = ''
-        Which port this service should listen on.
-      '';
-    };
-
-  };
-  default = {};
-  description = ''
-    Configuration for Foo, see
-    &lt;link xlink:href=&quot;https://example.com/docs/foo&quot;/&gt;
-    for supported values.
-  '';
-};
-</programlisting>
-    </section>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/development/sources.chapter.xml b/nixos/doc/manual/from_md/development/sources.chapter.xml
deleted file mode 100644
index aac18c9d06c..00000000000
--- a/nixos/doc/manual/from_md/development/sources.chapter.xml
+++ /dev/null
@@ -1,90 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-getting-sources">
-  <title>Getting the Sources</title>
-  <para>
-    By default, NixOS’s <literal>nixos-rebuild</literal> command uses
-    the NixOS and Nixpkgs sources provided by the
-    <literal>nixos</literal> channel (kept in
-    <literal>/nix/var/nix/profiles/per-user/root/channels/nixos</literal>).
-    To modify NixOS, however, you should check out the latest sources
-    from Git. This is as follows:
-  </para>
-  <programlisting>
-$ git clone https://github.com/NixOS/nixpkgs
-$ cd nixpkgs
-$ git remote update origin
-</programlisting>
-  <para>
-    This will check out the latest Nixpkgs sources to
-    <literal>./nixpkgs</literal> the NixOS sources to
-    <literal>./nixpkgs/nixos</literal>. (The NixOS source tree lives in
-    a subdirectory of the Nixpkgs repository.) The
-    <literal>nixpkgs</literal> repository has branches that correspond
-    to each Nixpkgs/NixOS channel (see <xref linkend="sec-upgrading" />
-    for more information about channels). Thus, the Git branch
-    <literal>origin/nixos-17.03</literal> will contain the latest built
-    and tested version available in the <literal>nixos-17.03</literal>
-    channel.
-  </para>
-  <para>
-    It’s often inconvenient to develop directly on the master branch,
-    since if somebody has just committed (say) a change to GCC, then the
-    binary cache may not have caught up yet and you’ll have to rebuild
-    everything from source. So you may want to create a local branch
-    based on your current NixOS version:
-  </para>
-  <programlisting>
-$ nixos-version
-17.09pre104379.6e0b727 (Hummingbird)
-
-$ git checkout -b local 6e0b727
-</programlisting>
-  <para>
-    Or, to base your local branch on the latest version available in a
-    NixOS channel:
-  </para>
-  <programlisting>
-$ git remote update origin
-$ git checkout -b local origin/nixos-17.03
-</programlisting>
-  <para>
-    (Replace <literal>nixos-17.03</literal> with the name of the channel
-    you want to use.) You can use <literal>git merge</literal> or
-    <literal>git rebase</literal> to keep your local branch in sync with
-    the channel, e.g.
-  </para>
-  <programlisting>
-$ git remote update origin
-$ git merge origin/nixos-17.03
-</programlisting>
-  <para>
-    You can use <literal>git cherry-pick</literal> to copy commits from
-    your local branch to the upstream branch.
-  </para>
-  <para>
-    If you want to rebuild your system using your (modified) sources,
-    you need to tell <literal>nixos-rebuild</literal> about them using
-    the <literal>-I</literal> flag:
-  </para>
-  <programlisting>
-# nixos-rebuild switch -I nixpkgs=/my/sources/nixpkgs
-</programlisting>
-  <para>
-    If you want <literal>nix-env</literal> to use the expressions in
-    <literal>/my/sources</literal>, use
-    <literal>nix-env -f /my/sources/nixpkgs</literal>, or change the
-    default by adding a symlink in <literal>~/.nix-defexpr</literal>:
-  </para>
-  <programlisting>
-$ ln -s /my/sources/nixpkgs ~/.nix-defexpr/nixpkgs
-</programlisting>
-  <para>
-    You may want to delete the symlink
-    <literal>~/.nix-defexpr/channels_root</literal> to prevent root’s
-    NixOS channel from clashing with your own tree (this may break the
-    command-not-found utility though). If you want to go back to the
-    default state, you may just remove the
-    <literal>~/.nix-defexpr</literal> directory completely, log out and
-    log in again and it should have been recreated with a link to the
-    root channels.
-  </para>
-</chapter>
diff --git a/nixos/doc/manual/from_md/development/testing-installer.chapter.xml b/nixos/doc/manual/from_md/development/testing-installer.chapter.xml
deleted file mode 100644
index 286d49f3c29..00000000000
--- a/nixos/doc/manual/from_md/development/testing-installer.chapter.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="ch-testing-installer">
-  <title>Testing the Installer</title>
-  <para>
-    Building, burning, and booting from an installation CD is rather
-    tedious, so here is a quick way to see if the installer works
-    properly:
-  </para>
-  <programlisting>
-# mount -t tmpfs none /mnt
-# nixos-generate-config --root /mnt
-$ nix-build '&lt;nixpkgs/nixos&gt;' -A config.system.build.nixos-install
-# ./result/bin/nixos-install
-</programlisting>
-  <para>
-    To start a login shell in the new NixOS installation in
-    <literal>/mnt</literal>:
-  </para>
-  <programlisting>
-$ nix-build '&lt;nixpkgs/nixos&gt;' -A config.system.build.nixos-enter
-# ./result/bin/nixos-enter
-</programlisting>
-</chapter>
diff --git a/nixos/doc/manual/from_md/development/unit-handling.section.xml b/nixos/doc/manual/from_md/development/unit-handling.section.xml
deleted file mode 100644
index 4c980e1213a..00000000000
--- a/nixos/doc/manual/from_md/development/unit-handling.section.xml
+++ /dev/null
@@ -1,131 +0,0 @@
-<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
deleted file mode 100644
index 66ba792ddac..00000000000
--- a/nixos/doc/manual/from_md/development/what-happens-during-a-system-switch.chapter.xml
+++ /dev/null
@@ -1,122 +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" 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-documentation.chapter.xml b/nixos/doc/manual/from_md/development/writing-documentation.chapter.xml
deleted file mode 100644
index 079c8006057..00000000000
--- a/nixos/doc/manual/from_md/development/writing-documentation.chapter.xml
+++ /dev/null
@@ -1,144 +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" xml:id="sec-writing-documentation">
-  <title>Writing NixOS Documentation</title>
-  <para>
-    As NixOS grows, so too does the need for a catalogue and explanation
-    of its extensive functionality. Collecting pertinent information
-    from disparate sources and presenting it in an accessible style
-    would be a worthy contribution to the project.
-  </para>
-  <section xml:id="sec-writing-docs-building-the-manual">
-    <title>Building the Manual</title>
-    <para>
-      The DocBook sources of the <xref linkend="book-nixos-manual" />
-      are in the
-      <link xlink:href="https://github.com/NixOS/nixpkgs/tree/master/nixos/doc/manual"><literal>nixos/doc/manual</literal></link>
-      subdirectory of the Nixpkgs repository.
-    </para>
-    <para>
-      You can quickly validate your edits with <literal>make</literal>:
-    </para>
-    <programlisting>
-$ cd /path/to/nixpkgs/nixos/doc/manual
-$ nix-shell
-nix-shell$ make
-</programlisting>
-    <para>
-      Once you are done making modifications to the manual, it's
-      important to build it before committing. You can do that as
-      follows:
-    </para>
-    <programlisting>
-nix-build nixos/release.nix -A manual.x86_64-linux
-</programlisting>
-    <para>
-      When this command successfully finishes, it will tell you where
-      the manual got generated. The HTML will be accessible through the
-      <literal>result</literal> symlink at
-      <literal>./result/share/doc/nixos/index.html</literal>.
-    </para>
-  </section>
-  <section xml:id="sec-writing-docs-editing-docbook-xml">
-    <title>Editing DocBook XML</title>
-    <para>
-      For general information on how to write in DocBook, see
-      <link xlink:href="http://www.docbook.org/tdg5/en/html/docbook.html">DocBook
-      5: The Definitive Guide</link>.
-    </para>
-    <para>
-      Emacs nXML Mode is very helpful for editing DocBook XML because it
-      validates the document as you write, and precisely locates errors.
-      To use it, see <xref linkend="sec-emacs-docbook-xml" />.
-    </para>
-    <para>
-      <link xlink:href="http://pandoc.org">Pandoc</link> can generate
-      DocBook XML from a multitude of formats, which makes a good
-      starting point. Here is an example of Pandoc invocation to convert
-      GitHub-Flavoured MarkDown to DocBook 5 XML:
-    </para>
-    <programlisting>
-pandoc -f markdown_github -t docbook5 docs.md -o my-section.md
-</programlisting>
-    <para>
-      Pandoc can also quickly convert a single
-      <literal>section.xml</literal> to HTML, which is helpful when
-      drafting.
-    </para>
-    <para>
-      Sometimes writing valid DocBook is simply too difficult. In this
-      case, submit your documentation updates in a
-      <link xlink:href="https://github.com/NixOS/nixpkgs/issues/new">GitHub
-      Issue</link> and someone will handle the conversion to XML for
-      you.
-    </para>
-  </section>
-  <section xml:id="sec-writing-docs-creating-a-topic">
-    <title>Creating a Topic</title>
-    <para>
-      You can use an existing topic as a basis for the new topic or
-      create a topic from scratch.
-    </para>
-    <para>
-      Keep the following guidelines in mind when you create and add a
-      topic:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The NixOS
-          <link xlink:href="http://www.docbook.org/tdg5/en/html/book.html"><literal>book</literal></link>
-          element is in <literal>nixos/doc/manual/manual.xml</literal>.
-          It includes several
-          <link xlink:href="http://www.docbook.org/tdg5/en/html/book.html"><literal>parts</literal></link>
-          which are in subdirectories.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Store the topic file in the same directory as the
-          <literal>part</literal> to which it belongs. If your topic is
-          about configuring a NixOS module, then the XML file can be
-          stored alongside the module definition <literal>nix</literal>
-          file.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          If you include multiple words in the file name, separate the
-          words with a dash. For example:
-          <literal>ipv6-config.xml</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Make sure that the <literal>xml:id</literal> value is unique.
-          You can use abbreviations if the ID is too long. For example:
-          <literal>nixos-config</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Determine whether your topic is a chapter or a section. If you
-          are unsure, open an existing topic file and check whether the
-          main element is chapter or section.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-writing-docs-adding-a-topic">
-    <title>Adding a Topic to the Book</title>
-    <para>
-      Open the parent XML file and add an <literal>xi:include</literal>
-      element to the list of chapters with the file name of the topic
-      that you created. If you created a <literal>section</literal>, you
-      add the file to the <literal>chapter</literal> file. If you
-      created a <literal>chapter</literal>, you add the file to the
-      <literal>part</literal> file.
-    </para>
-    <para>
-      If the topic is about configuring a NixOS module, it can be
-      automatically included in the manual by using the
-      <literal>meta.doc</literal> attribute. See
-      <xref linkend="sec-meta-attributes" /> for an explanation.
-    </para>
-  </section>
-</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
deleted file mode 100644
index 367731eda09..00000000000
--- a/nixos/doc/manual/from_md/development/writing-modules.chapter.xml
+++ /dev/null
@@ -1,245 +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" xml:id="sec-writing-modules">
-  <title>Writing NixOS Modules</title>
-  <para>
-    NixOS has a modular system for declarative configuration. This
-    system combines multiple <emphasis>modules</emphasis> to produce the
-    full system configuration. One of the modules that constitute the
-    configuration is <literal>/etc/nixos/configuration.nix</literal>.
-    Most of the others live in the
-    <link xlink:href="https://github.com/NixOS/nixpkgs/tree/master/nixos/modules"><literal>nixos/modules</literal></link>
-    subdirectory of the Nixpkgs tree.
-  </para>
-  <para>
-    Each NixOS module is a file that handles one logical aspect of the
-    configuration, such as a specific kind of hardware, a service, or
-    network settings. A module configuration does not have to handle
-    everything from scratch; it can use the functionality provided by
-    other modules for its implementation. Thus a module can
-    <emphasis>declare</emphasis> options that can be used by other
-    modules, and conversely can <emphasis>define</emphasis> options
-    provided by other modules in its own implementation. For example,
-    the module
-    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/security/pam.nix"><literal>pam.nix</literal></link>
-    declares the option <literal>security.pam.services</literal> that
-    allows other modules (e.g.
-    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/services/networking/ssh/sshd.nix"><literal>sshd.nix</literal></link>)
-    to define PAM services; and it defines the option
-    <literal>environment.etc</literal> (declared by
-    <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/system/etc/etc.nix"><literal>etc.nix</literal></link>)
-    to cause files to be created in <literal>/etc/pam.d</literal>.
-  </para>
-  <para>
-    In <xref linkend="sec-configuration-syntax" />, we saw the following
-    structure of NixOS modules:
-  </para>
-  <programlisting language="bash">
-{ config, pkgs, ... }:
-
-{ option definitions
-}
-</programlisting>
-  <para>
-    This is actually an <emphasis>abbreviated</emphasis> form of module
-    that only defines options, but does not declare any. The structure
-    of full NixOS modules is shown in
-    <link linkend="ex-module-syntax">Example: Structure of NixOS
-    Modules</link>.
-  </para>
-  <anchor xml:id="ex-module-syntax" />
-  <para>
-    <emphasis role="strong">Example: Structure of NixOS
-    Modules</emphasis>
-  </para>
-  <programlisting language="bash">
-{ config, pkgs, ... }:
-
-{
-  imports =
-    [ paths of other modules
-    ];
-
-  options = {
-    option declarations
-  };
-
-  config = {
-    option definitions
-  };
-}
-</programlisting>
-  <para>
-    The meaning of each part is as follows.
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        The first line makes the current Nix expression a function. The
-        variable <literal>pkgs</literal> contains Nixpkgs (by default,
-        it takes the <literal>nixpkgs</literal> entry of
-        <literal>NIX_PATH</literal>, see the
-        <link xlink:href="https://nixos.org/manual/nix/stable/#sec-common-env">Nix
-        manual</link> for further details), while
-        <literal>config</literal> contains the full system
-        configuration. This line can be omitted if there is no reference
-        to <literal>pkgs</literal> and <literal>config</literal> inside
-        the module.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        This <literal>imports</literal> list enumerates the paths to
-        other NixOS modules that should be included in the evaluation of
-        the system configuration. A default set of modules is defined in
-        the file <literal>modules/module-list.nix</literal>. These don't
-        need to be added in the import list.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The attribute <literal>options</literal> is a nested set of
-        <emphasis>option declarations</emphasis> (described below).
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The attribute <literal>config</literal> is a nested set of
-        <emphasis>option definitions</emphasis> (also described below).
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    <link linkend="locate-example">Example: NixOS Module for the
-    <quote>locate</quote> Service</link> shows a module that handles the
-    regular update of the <quote>locate</quote> database, an index of
-    all files in the file system. This module declares two options that
-    can be defined by other modules (typically the user’s
-    <literal>configuration.nix</literal>):
-    <literal>services.locate.enable</literal> (whether the database
-    should be updated) and <literal>services.locate.interval</literal>
-    (when the update should be done). It implements its functionality by
-    defining two options declared by other modules:
-    <literal>systemd.services</literal> (the set of all systemd
-    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
-    <quote>locate</quote> Service</emphasis>
-  </para>
-  <programlisting language="bash">
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  cfg = config.services.locate;
-in {
-  options.services.locate = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = ''
-        If enabled, NixOS will periodically update the database of
-        files used by the locate command.
-      '';
-    };
-
-    interval = mkOption {
-      type = types.str;
-      default = &quot;02:15&quot;;
-      example = &quot;hourly&quot;;
-      description = ''
-        Update the locate database at this interval. Updates by
-        default at 2:15 AM every day.
-
-        The format is described in
-        systemd.time(7).
-      '';
-    };
-
-    # Other options omitted for documentation
-  };
-
-  config = {
-    systemd.services.update-locatedb =
-      { description = &quot;Update Locate Database&quot;;
-        path  = [ pkgs.su ];
-        script =
-          ''
-            mkdir -m 0755 -p $(dirname ${toString cfg.output})
-            exec updatedb \
-              --localuser=${cfg.localuser} \
-              ${optionalString (!cfg.includeStore) &quot;--prunepaths='/nix/store'&quot;} \
-              --output=${toString cfg.output} ${concatStringsSep &quot; &quot; cfg.extraFlags}
-          '';
-      };
-
-    systemd.timers.update-locatedb = mkIf cfg.enable
-      { description = &quot;Update timer for locate database&quot;;
-        partOf      = [ &quot;update-locatedb.service&quot; ];
-        wantedBy    = [ &quot;timers.target&quot; ];
-        timerConfig.OnCalendar = cfg.interval;
-      };
-  };
-}
-</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" />
-  <xi:include href="assertions.section.xml" />
-  <xi:include href="meta-attributes.section.xml" />
-  <xi:include href="importing-modules.section.xml" />
-  <xi:include href="replace-modules.section.xml" />
-  <xi:include href="freeform-modules.section.xml" />
-  <xi:include href="settings-options.section.xml" />
-</chapter>
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
deleted file mode 100644
index 99bd37808c2..00000000000
--- a/nixos/doc/manual/from_md/development/writing-nixos-tests.section.xml
+++ /dev/null
@@ -1,769 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook"  xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" xml:id="sec-writing-nixos-tests">
-  <title>Writing Tests</title>
-  <para>
-    A NixOS test is a module that has the following structure:
-  </para>
-  <programlisting language="bash">
-{
-
-  # One or more machines:
-  nodes =
-    { machine =
-        { config, pkgs, ... }: { … };
-      machine2 =
-        { config, pkgs, ... }: { … };
-      …
-    };
-
-  testScript =
-    ''
-      Python code…
-    '';
-}
-</programlisting>
-  <para>
-    We refer to the whole test above as a test module, whereas the
-    values in
-    <link linkend="test-opt-nodes"><literal>nodes.&lt;name&gt;</literal></link>
-    are NixOS modules themselves.
-  </para>
-  <para>
-    The option
-    <link linkend="test-opt-testScript"><literal>testScript</literal></link>
-    is a piece 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 option
-    <link linkend="test-opt-nodes"><literal>nodes</literal></link>.
-  </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>
-  <section xml:id="sec-calling-nixos-tests">
-    <title>Calling a test</title>
-    <para>
-      Tests are invoked differently depending on whether the test is
-      part of NixOS or lives in a different project.
-    </para>
-    <section xml:id="sec-call-nixos-test-in-nixos">
-      <title>Testing within NixOS</title>
-      <para>
-        Tests that are part of NixOS are added to
-        <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/all-tests.nix"><literal>nixos/tests/all-tests.nix</literal></link>.
-      </para>
-      <programlisting language="bash">
-  hostname = runTest ./hostname.nix;
-</programlisting>
-      <para>
-        Overrides can be added by defining an anonymous module in
-        <literal>all-tests.nix</literal>.
-      </para>
-      <programlisting language="bash">
-  hostname = runTest {
-    imports = [ ./hostname.nix ];
-    defaults.networking.firewall.enable = false;
-  };
-</programlisting>
-      <para>
-        You can run a test with attribute name
-        <literal>hostname</literal> in
-        <literal>nixos/tests/all-tests.nix</literal> by invoking:
-      </para>
-      <programlisting>
-cd /my/git/clone/of/nixpkgs
-nix-build -A nixosTests.hostname
-</programlisting>
-    </section>
-    <section xml:id="sec-call-nixos-test-outside-nixos">
-      <title>Testing outside the NixOS project</title>
-      <para>
-        Outside the <literal>nixpkgs</literal> repository, you can
-        instantiate the test by first importing the NixOS library,
-      </para>
-      <programlisting language="bash">
-let nixos-lib = import (nixpkgs + &quot;/nixos/lib&quot;) { };
-in
-
-nixos-lib.runTest {
-  imports = [ ./test.nix ];
-  hostPkgs = pkgs;  # the Nixpkgs package set used outside the VMs
-  defaults.services.foo.package = mypkg;
-}
-</programlisting>
-      <para>
-        <literal>runTest</literal> returns a derivation that runs the
-        test.
-      </para>
-    </section>
-  </section>
-  <section xml:id="sec-nixos-test-nodes">
-    <title>Configuring the nodes</title>
-    <para>
-      There are a few special NixOS options for test VMs:
-    </para>
-    <variablelist>
-      <varlistentry>
-        <term>
-          <literal>virtualisation.memorySize</literal>
-        </term>
-        <listitem>
-          <para>
-            The memory of the VM in megabytes.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>virtualisation.vlans</literal>
-        </term>
-        <listitem>
-          <para>
-            The virtual networks to which the VM is connected. See
-            <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/tests/nat.nix"><literal>nat.nix</literal></link>
-            for an example.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>virtualisation.writableStore</literal>
-        </term>
-        <listitem>
-          <para>
-            By default, the Nix store in the VM is not writable. If you
-            enable this option, a writable union file system is mounted
-            on top of the Nix store to make it appear writable. This is
-            necessary for tests that run Nix operations that modify the
-            store.
-          </para>
-        </listitem>
-      </varlistentry>
-    </variablelist>
-    <para>
-      For more options, see the module
-      <link xlink:href="https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/virtualisation/qemu-vm.nix"><literal>qemu-vm.nix</literal></link>.
-    </para>
-    <para>
-      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 <literal>name</literal> if this is also the
-      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:
-    </para>
-    <programlisting language="python">
-machine.start()
-machine.wait_for_unit(&quot;default.target&quot;)
-if not &quot;Linux&quot; in machine.succeed(&quot;uname&quot;):
-  raise Exception(&quot;Wrong OS&quot;)
-</programlisting>
-    <para>
-      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 them in parallel:
-    </para>
-    <programlisting language="python">
-start_all()
-</programlisting>
-  </section>
-  <section xml:id="ssec-machine-objects">
-    <title>Machine objects</title>
-    <para>
-      The following methods are available on machine objects:
-    </para>
-    <variablelist>
-      <varlistentry>
-        <term>
-          <literal>start</literal>
-        </term>
-        <listitem>
-          <para>
-            Start the virtual machine. This method is asynchronous — it
-            does not wait for the machine to finish booting.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>shutdown</literal>
-        </term>
-        <listitem>
-          <para>
-            Shut down the machine, waiting for the VM to exit.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>crash</literal>
-        </term>
-        <listitem>
-          <para>
-            Simulate a sudden power failure, by telling the VM to exit
-            immediately.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>block</literal>
-        </term>
-        <listitem>
-          <para>
-            Simulate unplugging the Ethernet cable that connects the
-            machine to the other machines.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>unblock</literal>
-        </term>
-        <listitem>
-          <para>
-            Undo the effect of <literal>block</literal>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>screenshot</literal>
-        </term>
-        <listitem>
-          <para>
-            Take a picture of the display of the virtual machine, in PNG
-            format. The screenshot is linked from the HTML log.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>get_screen_text_variants</literal>
-        </term>
-        <listitem>
-          <para>
-            Return a list of different interpretations of what is
-            currently visible on the machine's screen using optical
-            character recognition. The number and order of the
-            interpretations is not specified and is subject to change,
-            but if no exception is raised at least one will be returned.
-          </para>
-          <note>
-            <para>
-              This requires
-              <link linkend="test-opt-enableOCR"><literal>enableOCR</literal></link>
-              to be set to <literal>true</literal>.
-            </para>
-          </note>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>get_screen_text</literal>
-        </term>
-        <listitem>
-          <para>
-            Return a textual representation of what is currently visible
-            on the machine's screen using optical character recognition.
-          </para>
-          <note>
-            <para>
-              This requires
-              <link linkend="test-opt-enableOCR"><literal>enableOCR</literal></link>
-              to be set to <literal>true</literal>.
-            </para>
-          </note>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>send_monitor_command</literal>
-        </term>
-        <listitem>
-          <para>
-            Send a command to the QEMU monitor. This is rarely used, but
-            allows doing stuff such as attaching virtual USB disks to a
-            running machine.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>send_key</literal>
-        </term>
-        <listitem>
-          <para>
-            Simulate pressing keys on the virtual keyboard, e.g.,
-            <literal>send_key(&quot;ctrl-alt-delete&quot;)</literal>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>send_chars</literal>
-        </term>
-        <listitem>
-          <para>
-            Simulate typing a sequence of characters on the virtual
-            keyboard, e.g.,
-            <literal>send_chars(&quot;foobar\n&quot;)</literal> will
-            type the string <literal>foobar</literal> followed by the
-            Enter key.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>send_console</literal>
-        </term>
-        <listitem>
-          <para>
-            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>execute</literal>
-        </term>
-        <listitem>
-          <para>
-            Execute a shell command, returning a list
-            <literal>(status, stdout)</literal>.
-          </para>
-          <para>
-            Commands are run with <literal>set -euo pipefail</literal>
-            set:
-          </para>
-          <itemizedlist>
-            <listitem>
-              <para>
-                If several commands are separated by
-                <literal>;</literal> and one fails, the command as a
-                whole will fail.
-              </para>
-            </listitem>
-            <listitem>
-              <para>
-                For pipelines, the last non-zero exit status will be
-                returned (if there is one; otherwise zero will be
-                returned).
-              </para>
-            </listitem>
-            <listitem>
-              <para>
-                Dereferencing unset variables fails the command.
-              </para>
-            </listitem>
-            <listitem>
-              <para>
-                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>
-        <term>
-          <literal>fail</literal>
-        </term>
-        <listitem>
-          <para>
-            Like <literal>succeed</literal>, but raising an exception if
-            the command returns a zero status.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>wait_until_succeeds</literal>
-        </term>
-        <listitem>
-          <para>
-            Repeat a shell command with 1-second intervals until it
-            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>
-      <varlistentry>
-        <term>
-          <literal>wait_until_fails</literal>
-        </term>
-        <listitem>
-          <para>
-            Like <literal>wait_until_succeeds</literal>, but repeating
-            the command until it fails.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>wait_for_unit</literal>
-        </term>
-        <listitem>
-          <para>
-            Wait until the specified systemd unit has reached the
-            <quote>active</quote> state.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>wait_for_file</literal>
-        </term>
-        <listitem>
-          <para>
-            Wait until the specified file exists.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>wait_for_open_port</literal>
-        </term>
-        <listitem>
-          <para>
-            Wait until a process is listening on the given TCP port (on
-            <literal>localhost</literal>, at least).
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>wait_for_closed_port</literal>
-        </term>
-        <listitem>
-          <para>
-            Wait until nobody is listening on the given TCP port.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>wait_for_x</literal>
-        </term>
-        <listitem>
-          <para>
-            Wait until the X11 server is accepting connections.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>wait_for_text</literal>
-        </term>
-        <listitem>
-          <para>
-            Wait until the supplied regular expressions matches the
-            textual contents of the screen by using optical character
-            recognition (see <literal>get_screen_text</literal> and
-            <literal>get_screen_text_variants</literal>).
-          </para>
-          <note>
-            <para>
-              This requires
-              <link linkend="test-opt-enableOCR"><literal>enableOCR</literal></link>
-              to be set to <literal>true</literal>.
-            </para>
-          </note>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>wait_for_console_text</literal>
-        </term>
-        <listitem>
-          <para>
-            Wait until the supplied regular expressions match a line of
-            the serial console output. This method is useful when OCR is
-            not possible or accurate enough.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>wait_for_window</literal>
-        </term>
-        <listitem>
-          <para>
-            Wait until an X11 window has appeared whose name matches the
-            given regular expression, e.g.,
-            <literal>wait_for_window(&quot;Terminal&quot;)</literal>.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>copy_from_host</literal>
-        </term>
-        <listitem>
-          <para>
-            Copies a file from host to machine, e.g.,
-            <literal>copy_from_host(&quot;myfile&quot;, &quot;/etc/my/important/file&quot;)</literal>.
-          </para>
-          <para>
-            The first argument is the file on the host. The file needs
-            to be accessible while building the nix derivation. The
-            second argument is the location of the file on the machine.
-          </para>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>systemctl</literal>
-        </term>
-        <listitem>
-          <para>
-            Runs <literal>systemctl</literal> commands with optional
-            support for <literal>systemctl --user</literal>
-          </para>
-          <programlisting language="python">
-machine.systemctl(&quot;list-jobs --no-pager&quot;) # runs `systemctl list-jobs --no-pager`
-machine.systemctl(&quot;list-jobs --no-pager&quot;, &quot;any-user&quot;) # spawns a shell for `any-user` and runs `systemctl --user list-jobs --no-pager`
-</programlisting>
-        </listitem>
-      </varlistentry>
-      <varlistentry>
-        <term>
-          <literal>shell_interact</literal>
-        </term>
-        <listitem>
-          <para>
-            Allows you to directly interact with the guest shell. This
-            should only be used during test development, not in
-            production tests. Killing the interactive session with
-            <literal>Ctrl-d</literal> or <literal>Ctrl-c</literal> also
-            ends the guest session.
-          </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
-      <literal>systemd.user.services</literal> the optional
-      <literal>user</literal> argument can be used:
-    </para>
-    <programlisting language="python">
-machine.start()
-machine.wait_for_x()
-machine.wait_for_unit(&quot;xautolock.service&quot;, &quot;x-session-user&quot;)
-</programlisting>
-    <para>
-      This applies to <literal>systemctl</literal>,
-      <literal>get_unit_info</literal>,
-      <literal>wait_for_unit</literal>, <literal>start_job</literal> and
-      <literal>stop_job</literal>.
-    </para>
-    <para>
-      For faster dev cycles it's also possible to disable the
-      code-linters (this shouldn't be committed though):
-    </para>
-    <programlisting language="bash">
-{
-  skipLint = true;
-  nodes.machine =
-    { config, pkgs, ... }:
-    { configuration…
-    };
-
-  testScript =
-    ''
-      Python code…
-    '';
-}
-</programlisting>
-    <para>
-      This will produce a Nix warning at evaluation time. To fully
-      disable the linter, wrap the test script in comment directives to
-      disable the Black linter directly (again, don't commit this within
-      the Nixpkgs repository):
-    </para>
-    <programlisting language="bash">
-  testScript =
-    ''
-      # fmt: off
-      Python code…
-      # fmt: on
-    '';
-</programlisting>
-    <para>
-      Similarly, the type checking of test scripts can be disabled in
-      the following way:
-    </para>
-    <programlisting language="bash">
-{
-  skipTypeCheck = true;
-  nodes.machine =
-    { config, pkgs, ... }:
-    { configuration…
-    };
-}
-</programlisting>
-  </section>
-  <section xml:id="ssec-failing-tests-early">
-    <title>Failing tests early</title>
-    <para>
-      To fail tests early when certain invariants are no longer met
-      (instead of waiting for the build to time out), the decorator
-      <literal>polling_condition</literal> is provided. For example, if
-      we are testing a program <literal>foo</literal> that should not
-      quit after being started, we might write the following:
-    </para>
-    <programlisting language="python">
-@polling_condition
-def foo_running():
-    machine.succeed(&quot;pgrep -x foo&quot;)
-
-
-machine.succeed(&quot;foo --start&quot;)
-machine.wait_until_succeeds(&quot;pgrep -x foo&quot;)
-
-with foo_running:
-    ...  # Put `foo` through its paces
-</programlisting>
-    <para>
-      <literal>polling_condition</literal> takes the following
-      (optional) arguments:
-    </para>
-    <para>
-      <literal>seconds_interval</literal>
-    </para>
-    <para>
-      : specifies how often the condition should be polled:
-    </para>
-    <programlisting language="python">
-@polling_condition(seconds_interval=10)
-def foo_running():
-    machine.succeed(&quot;pgrep -x foo&quot;)
-</programlisting>
-    <para>
-      <literal>description</literal>
-    </para>
-    <para>
-      : is used in the log when the condition is checked. If this is not
-      provided, the description is pulled from the docstring of the
-      function. These two are therefore equivalent:
-    </para>
-    <programlisting language="python">
-@polling_condition
-def foo_running():
-    &quot;check that foo is running&quot;
-    machine.succeed(&quot;pgrep -x foo&quot;)
-</programlisting>
-    <programlisting language="python">
-@polling_condition(description=&quot;check that foo is running&quot;)
-def foo_running():
-    machine.succeed(&quot;pgrep -x foo&quot;)
-</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">
-{
-  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 xml:id="sec-test-options-reference">
-    <title>Test Options Reference</title>
-    <para>
-      The following options can be used when writing tests.
-    </para>
-    <xi:include href="../../generated/test-options-db.xml" xpointer="test-options-list"/>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/installation/building-nixos.chapter.xml b/nixos/doc/manual/from_md/installation/building-nixos.chapter.xml
deleted file mode 100644
index 080f1535e41..00000000000
--- a/nixos/doc/manual/from_md/installation/building-nixos.chapter.xml
+++ /dev/null
@@ -1,111 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-building-image">
-  <title>Building a NixOS (Live) ISO</title>
-  <para>
-    Default live installer configurations are available inside
-    <literal>nixos/modules/installer/cd-dvd</literal>. For building
-    other system images,
-    <link xlink:href="https://github.com/nix-community/nixos-generators">nixos-generators</link>
-    is a good place to start looking at.
-  </para>
-  <para>
-    You have two options:
-  </para>
-  <itemizedlist spacing="compact">
-    <listitem>
-      <para>
-        Use any of those default configurations as is
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Combine them with (any of) your host config(s)
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    System images, such as the live installer ones, know how to enforce
-    configuration settings on which they immediately depend in order to
-    work correctly.
-  </para>
-  <para>
-    However, if you are confident, you can opt to override those
-    enforced values with <literal>mkForce</literal>.
-  </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>
-      To check the content of an ISO image, mount it like so:
-    </para>
-    <programlisting>
-# 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>
-      The config value enforcement is implemented via
-      <literal>mkImageMediaOverride = mkOverride 60;</literal> and
-      therefore primes over simple value assignments, but also yields to
-      <literal>mkForce</literal>.
-    </para>
-    <para>
-      This property allows image designers to implement in semantically
-      correct ways those configuration values upon which the correct
-      functioning of the image depends.
-    </para>
-    <para>
-      For example, the iso base image overrides those file systems which
-      it needs at a minimum for correct functioning, while the installer
-      base image overrides the entire file system layout because there
-      can’t be any other guarantees on a live medium than those given by
-      the live medium itself. The latter is especially true before
-      formatting the target block device(s). On the other hand, the
-      netboot iso only overrides its minimum dependencies since netboot
-      images are always made-to-target.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/installation/changing-config.chapter.xml b/nixos/doc/manual/from_md/installation/changing-config.chapter.xml
deleted file mode 100644
index 86f0b15b41c..00000000000
--- a/nixos/doc/manual/from_md/installation/changing-config.chapter.xml
+++ /dev/null
@@ -1,117 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-changing-config">
-  <title>Changing the Configuration</title>
-  <para>
-    The file <literal>/etc/nixos/configuration.nix</literal> contains
-    the current configuration of your machine. Whenever you’ve
-    <link linkend="ch-configuration">changed something</link> in that
-    file, you should do
-  </para>
-  <programlisting>
-# nixos-rebuild switch
-</programlisting>
-  <para>
-    to build the new configuration, make it the default configuration
-    for booting, and try to realise the configuration in the running
-    system (e.g., by restarting system services).
-  </para>
-  <warning>
-    <para>
-      This command doesn't start/stop
-      <link linkend="opt-systemd.user.services">user services</link>
-      automatically. <literal>nixos-rebuild</literal> only runs a
-      <literal>daemon-reload</literal> for each user with running user
-      services.
-    </para>
-  </warning>
-  <warning>
-    <para>
-      These commands must be executed as root, so you should either run
-      them from a root shell or by prefixing them with
-      <literal>sudo -i</literal>.
-    </para>
-  </warning>
-  <para>
-    You can also do
-  </para>
-  <programlisting>
-# nixos-rebuild test
-</programlisting>
-  <para>
-    to build the configuration and switch the running system to it, but
-    without making it the boot default. So if (say) the configuration
-    locks up your machine, you can just reboot to get back to a working
-    configuration.
-  </para>
-  <para>
-    There is also
-  </para>
-  <programlisting>
-# nixos-rebuild boot
-</programlisting>
-  <para>
-    to build the configuration and make it the boot default, but not
-    switch to it now (so it will only take effect after the next
-    reboot).
-  </para>
-  <para>
-    You can make your configuration show up in a different submenu of
-    the GRUB 2 boot screen by giving it a different <emphasis>profile
-    name</emphasis>, e.g.
-  </para>
-  <programlisting>
-# nixos-rebuild switch -p test
-</programlisting>
-  <para>
-    which causes the new configuration (and previous ones created using
-    <literal>-p test</literal>) to show up in the GRUB submenu
-    <quote>NixOS - Profile 'test'</quote>. This can be useful to
-    separate test configurations from <quote>stable</quote>
-    configurations.
-  </para>
-  <para>
-    Finally, you can do
-  </para>
-  <programlisting>
-$ nixos-rebuild build
-</programlisting>
-  <para>
-    to build the configuration but nothing more. This is useful to see
-    whether everything compiles cleanly.
-  </para>
-  <para>
-    If you have a machine that supports hardware virtualisation, you can
-    also test the new configuration in a sandbox by building and running
-    a QEMU <emphasis>virtual machine</emphasis> that contains the
-    desired configuration. Just do
-  </para>
-  <programlisting>
-$ nixos-rebuild build-vm
-$ ./result/bin/run-*-vm
-</programlisting>
-  <para>
-    The VM does not have any data from your host system, so your
-    existing user accounts and home directories will not be available
-    unless you have set <literal>mutableUsers = false</literal>. Another
-    way is to temporarily add the following to your configuration:
-  </para>
-  <programlisting language="bash">
-users.users.your-user.initialHashedPassword = &quot;test&quot;;
-</programlisting>
-  <para>
-    <emphasis>Important:</emphasis> delete the $hostname.qcow2 file if
-    you have started the virtual machine at least once without the right
-    users, otherwise the changes will not get picked up. You can forward
-    ports on the host to the guest. For instance, the following will
-    forward host port 2222 to guest port 22 (SSH):
-  </para>
-  <programlisting>
-$ QEMU_NET_OPTS=&quot;hostfwd=tcp::2222-:22&quot; ./result/bin/run-*-vm
-</programlisting>
-  <para>
-    allowing you to log in via SSH (assuming you have set the
-    appropriate passwords or SSH authorized keys):
-  </para>
-  <programlisting>
-$ ssh -p 2222 localhost
-</programlisting>
-</chapter>
diff --git a/nixos/doc/manual/from_md/installation/installing-behind-a-proxy.section.xml b/nixos/doc/manual/from_md/installation/installing-behind-a-proxy.section.xml
deleted file mode 100644
index a551807cd47..00000000000
--- a/nixos/doc/manual/from_md/installation/installing-behind-a-proxy.section.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-installing-behind-proxy">
-  <title>Installing behind a proxy</title>
-  <para>
-    To install NixOS behind a proxy, do the following before running
-    <literal>nixos-install</literal>.
-  </para>
-  <orderedlist numeration="arabic">
-    <listitem>
-      <para>
-        Update proxy configuration in
-        <literal>/mnt/etc/nixos/configuration.nix</literal> to keep the
-        internet accessible after reboot.
-      </para>
-      <programlisting language="bash">
-networking.proxy.default = &quot;http://user:password@proxy:port/&quot;;
-networking.proxy.noProxy = &quot;127.0.0.1,localhost,internal.domain&quot;;
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        Setup the proxy environment variables in the shell where you are
-        running <literal>nixos-install</literal>.
-      </para>
-      <programlisting>
-# proxy_url=&quot;http://user:password@proxy:port/&quot;
-# export http_proxy=&quot;$proxy_url&quot;
-# export HTTP_PROXY=&quot;$proxy_url&quot;
-# export https_proxy=&quot;$proxy_url&quot;
-# export HTTPS_PROXY=&quot;$proxy_url&quot;
-</programlisting>
-    </listitem>
-  </orderedlist>
-  <note>
-    <para>
-      If you are switching networks with different proxy configurations,
-      use the <literal>specialisation</literal> option in
-      <literal>configuration.nix</literal> to switch proxies at runtime.
-      Refer to <xref linkend="ch-options" /> for more information.
-    </para>
-  </note>
-</section>
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
deleted file mode 100644
index f29200952ac..00000000000
--- a/nixos/doc/manual/from_md/installation/installing-from-other-distro.section.xml
+++ /dev/null
@@ -1,388 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-installing-from-other-distro">
-  <title>Installing from another Linux distribution</title>
-  <para>
-    Because Nix (the package manager) &amp; Nixpkgs (the Nix packages
-    collection) can both be installed on any (most?) Linux
-    distributions, they can be used to install NixOS in various creative
-    ways. You can, for instance:
-  </para>
-  <orderedlist numeration="arabic">
-    <listitem>
-      <para>
-        Install NixOS on another partition, from your existing Linux
-        distribution (without the use of a USB or optical device!)
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Install NixOS on the same partition (in place!), from your
-        existing non-NixOS Linux distribution using
-        <literal>NIXOS_LUSTRATE</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Install NixOS on your hard drive from the Live CD of any Linux
-        distribution.
-      </para>
-    </listitem>
-  </orderedlist>
-  <para>
-    The first steps to all these are the same:
-  </para>
-  <orderedlist numeration="arabic">
-    <listitem>
-      <para>
-        Install the Nix package manager:
-      </para>
-      <para>
-        Short version:
-      </para>
-      <programlisting>
-$ curl -L https://nixos.org/nix/install | sh
-$ . $HOME/.nix-profile/etc/profile.d/nix.sh # …or open a fresh shell
-</programlisting>
-      <para>
-        More details in the
-        <link xlink:href="https://nixos.org/nix/manual/#chap-quick-start">
-        Nix manual</link>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Switch to the NixOS channel:
-      </para>
-      <para>
-        If you've just installed Nix on a non-NixOS distribution, you
-        will be on the <literal>nixpkgs</literal> channel by default.
-      </para>
-      <programlisting>
-$ nix-channel --list
-nixpkgs https://nixos.org/channels/nixpkgs-unstable
-</programlisting>
-      <para>
-        As that channel gets released without running the NixOS tests,
-        it will be safer to use the <literal>nixos-*</literal> channels
-        instead:
-      </para>
-      <programlisting>
-$ nix-channel --add https://nixos.org/channels/nixos-version nixpkgs
-</programlisting>
-      <para>
-        You may want to throw in a
-        <literal>nix-channel --update</literal> for good measure.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Install the NixOS installation tools:
-      </para>
-      <para>
-        You'll need <literal>nixos-generate-config</literal> and
-        <literal>nixos-install</literal>, but this also makes some man
-        pages and <literal>nixos-enter</literal> available, just in case
-        you want to chroot into your NixOS partition. NixOS installs
-        these by default, but you don't have NixOS yet..
-      </para>
-      <programlisting>
-$ nix-env -f '&lt;nixpkgs&gt;' -iA nixos-install-tools
-</programlisting>
-    </listitem>
-    <listitem>
-      <note>
-        <para>
-          The following 5 steps are only for installing NixOS to another
-          partition. For installing NixOS in place using
-          <literal>NIXOS_LUSTRATE</literal>, skip ahead.
-        </para>
-      </note>
-      <para>
-        Prepare your target partition:
-      </para>
-      <para>
-        At this point it is time to prepare your target partition.
-        Please refer to the partitioning, file-system creation, and
-        mounting steps of <xref linkend="sec-installation" />
-      </para>
-      <para>
-        If you're about to install NixOS in place using
-        <literal>NIXOS_LUSTRATE</literal> there is nothing to do for
-        this step.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Generate your NixOS configuration:
-      </para>
-      <programlisting>
-$ sudo `which nixos-generate-config` --root /mnt
-</programlisting>
-      <para>
-        You'll probably want to edit the configuration files. Refer to
-        the <literal>nixos-generate-config</literal> step in
-        <xref linkend="sec-installation" /> for more information.
-      </para>
-      <para>
-        Consider setting up the NixOS bootloader to give you the ability
-        to boot on your existing Linux partition. For instance, if
-        you're using GRUB and your existing distribution is running
-        Ubuntu, you may want to add something like this to your
-        <literal>configuration.nix</literal>:
-      </para>
-      <programlisting language="bash">
-boot.loader.grub.extraEntries = ''
-  menuentry &quot;Ubuntu&quot; {
-    search --set=ubuntu --fs-uuid 3cc3e652-0c1f-4800-8451-033754f68e6e
-    configfile &quot;($ubuntu)/boot/grub/grub.cfg&quot;
-  }
-'';
-</programlisting>
-      <para>
-        (You can find the appropriate UUID for your partition in
-        <literal>/dev/disk/by-uuid</literal>)
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Create the <literal>nixbld</literal> group and user on your
-        original distribution:
-      </para>
-      <programlisting>
-$ sudo groupadd -g 30000 nixbld
-$ sudo useradd -u 30000 -g nixbld -G nixbld nixbld
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        Download/build/install NixOS:
-      </para>
-      <warning>
-        <para>
-          Once you complete this step, you might no longer be able to
-          boot on existing systems without the help of a rescue USB
-          drive or similar.
-        </para>
-      </warning>
-      <note>
-        <para>
-          On some distributions there are separate PATHS for programs
-          intended only for root. In order for the installation to
-          succeed, you might have to use
-          <literal>PATH=&quot;$PATH:/usr/sbin:/sbin&quot;</literal> in
-          the following command.
-        </para>
-      </note>
-      <programlisting>
-$ sudo PATH=&quot;$PATH&quot; NIX_PATH=&quot;$NIX_PATH&quot; `which nixos-install` --root /mnt
-</programlisting>
-      <para>
-        Again, please refer to the <literal>nixos-install</literal> step
-        in <xref linkend="sec-installation" /> for more information.
-      </para>
-      <para>
-        That should be it for installation to another partition!
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Optionally, you may want to clean up your non-NixOS
-        distribution:
-      </para>
-      <programlisting>
-$ sudo userdel nixbld
-$ sudo groupdel nixbld
-</programlisting>
-      <para>
-        If you do not wish to keep the Nix package manager installed
-        either, run something like
-        <literal>sudo rm -rv ~/.nix-* /nix</literal> and remove the line
-        that the Nix installer added to your
-        <literal>~/.profile</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <note>
-        <para>
-          The following steps are only for installing NixOS in place
-          using <literal>NIXOS_LUSTRATE</literal>:
-        </para>
-      </note>
-      <para>
-        Generate your NixOS configuration:
-      </para>
-      <programlisting>
-$ sudo `which nixos-generate-config`
-</programlisting>
-      <para>
-        Note that this will place the generated configuration files in
-        <literal>/etc/nixos</literal>. You'll probably want to edit the
-        configuration files. Refer to the
-        <literal>nixos-generate-config</literal> step in
-        <xref linkend="sec-installation" /> for more information.
-      </para>
-      <para>
-        You'll likely want to set a root password for your first boot
-        using the configuration files because you won't have a chance to
-        enter a password until after you reboot. You can initialize the
-        root password to an empty one with this line: (and of course
-        don't forget to set one once you've rebooted or to lock the
-        account with <literal>sudo passwd -l root</literal> if you use
-        <literal>sudo</literal>)
-      </para>
-      <programlisting language="bash">
-users.users.root.initialHashedPassword = &quot;&quot;;
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        Build the NixOS closure and install it in the
-        <literal>system</literal> profile:
-      </para>
-      <programlisting>
-$ nix-env -p /nix/var/nix/profiles/system -f '&lt;nixpkgs/nixos&gt;' -I nixos-config=/etc/nixos/configuration.nix -iA system
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        Change ownership of the <literal>/nix</literal> tree to root
-        (since your Nix install was probably single user):
-      </para>
-      <programlisting>
-$ sudo chown -R 0:0 /nix
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        Set up the <literal>/etc/NIXOS</literal> and
-        <literal>/etc/NIXOS_LUSTRATE</literal> files:
-      </para>
-      <para>
-        <literal>/etc/NIXOS</literal> officializes that this is now a
-        NixOS partition (the bootup scripts require its presence).
-      </para>
-      <para>
-        <literal>/etc/NIXOS_LUSTRATE</literal> tells the NixOS bootup
-        scripts to move <emphasis>everything</emphasis> that's in the
-        root partition to <literal>/old-root</literal>. This will move
-        your existing distribution out of the way in the very early
-        stages of the NixOS bootup. There are exceptions (we do need to
-        keep NixOS there after all), so the NixOS lustrate process will
-        not touch:
-      </para>
-      <itemizedlist>
-        <listitem>
-          <para>
-            The <literal>/nix</literal> directory
-          </para>
-        </listitem>
-        <listitem>
-          <para>
-            The <literal>/boot</literal> directory
-          </para>
-        </listitem>
-        <listitem>
-          <para>
-            Any file or directory listed in
-            <literal>/etc/NIXOS_LUSTRATE</literal> (one per line)
-          </para>
-        </listitem>
-      </itemizedlist>
-      <note>
-        <para>
-          Support for <literal>NIXOS_LUSTRATE</literal> was added in
-          NixOS 16.09. The act of &quot;lustrating&quot; refers to the
-          wiping of the existing distribution. Creating
-          <literal>/etc/NIXOS_LUSTRATE</literal> can also be used on
-          NixOS to remove all mutable files from your root partition
-          (anything that's not in <literal>/nix</literal> or
-          <literal>/boot</literal> gets &quot;lustrated&quot; on the
-          next boot.
-        </para>
-        <para>
-          lustrate /ˈlʌstreɪt/ verb.
-        </para>
-        <para>
-          purify by expiatory sacrifice, ceremonial washing, or some
-          other ritual action.
-        </para>
-      </note>
-      <para>
-        Let's create the files:
-      </para>
-      <programlisting>
-$ sudo touch /etc/NIXOS
-$ sudo touch /etc/NIXOS_LUSTRATE
-</programlisting>
-      <para>
-        Let's also make sure the NixOS configuration files are kept once
-        we reboot on NixOS:
-      </para>
-      <programlisting>
-$ echo etc/nixos | sudo tee -a /etc/NIXOS_LUSTRATE
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        Finally, move the <literal>/boot</literal> directory of your
-        current distribution out of the way (the lustrate process will
-        take care of the rest once you reboot, but this one must be
-        moved out now because NixOS needs to install its own boot files:
-      </para>
-      <warning>
-        <para>
-          Once you complete this step, your current distribution will no
-          longer be bootable! If you didn't get all the NixOS
-          configuration right, especially those settings pertaining to
-          boot loading and root partition, NixOS may not be bootable
-          either. Have a USB rescue device ready in case this happens.
-        </para>
-      </warning>
-      <programlisting>
-$ sudo mv -v /boot /boot.bak &amp;&amp;
-sudo /nix/var/nix/profiles/system/bin/switch-to-configuration boot
-</programlisting>
-      <para>
-        Cross your fingers, reboot, hopefully you should get a NixOS
-        prompt!
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        If for some reason you want to revert to the old distribution,
-        you'll need to boot on a USB rescue disk and do something along
-        these lines:
-      </para>
-      <programlisting>
-# mkdir root
-# mount /dev/sdaX root
-# mkdir root/nixos-root
-# mv -v root/* root/nixos-root/
-# mv -v root/nixos-root/old-root/* root/
-# mv -v root/boot.bak root/boot  # We had renamed this by hand earlier
-# umount root
-# reboot
-</programlisting>
-      <para>
-        This may work as is or you might also need to reinstall the boot
-        loader.
-      </para>
-      <para>
-        And of course, if you're happy with NixOS and no longer need the
-        old distribution:
-      </para>
-      <programlisting>
-sudo rm -rf /old-root
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        It's also worth noting that this whole process can be automated.
-        This is especially useful for Cloud VMs, where provider do not
-        provide NixOS. For instance,
-        <link xlink:href="https://github.com/elitak/nixos-infect">nixos-infect</link>
-        uses the lustrate process to convert Digital Ocean droplets to
-        NixOS from other distributions automatically.
-      </para>
-    </listitem>
-  </orderedlist>
-</section>
diff --git a/nixos/doc/manual/from_md/installation/installing-kexec.section.xml b/nixos/doc/manual/from_md/installation/installing-kexec.section.xml
deleted file mode 100644
index 46ea0d59b6c..00000000000
--- a/nixos/doc/manual/from_md/installation/installing-kexec.section.xml
+++ /dev/null
@@ -1,94 +0,0 @@
-<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
deleted file mode 100644
index 94172de65ea..00000000000
--- a/nixos/doc/manual/from_md/installation/installing-pxe.section.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-booting-from-pxe">
-  <title>Booting from the <quote>netboot</quote> media (PXE)</title>
-  <para>
-    Advanced users may wish to install NixOS using an existing PXE or
-    iPXE setup.
-  </para>
-  <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 your current version of
-    nixpkgs, you can run:
-  </para>
-  <programlisting>
-nix-build -A netboot.x86_64-linux '&lt;nixpkgs/nixos/release.nix&gt;'
-</programlisting>
-  <para>
-    This will create a <literal>result</literal> directory containing: *
-    <literal>bzImage</literal> – the Linux kernel *
-    <literal>initrd</literal> – the initrd file *
-    <literal>netboot.ipxe</literal> – an example ipxe script
-    demonstrating the appropriate kernel command line arguments for this
-    image
-  </para>
-  <para>
-    If you’re using plain PXE, configure your boot loader to use the
-    <literal>bzImage</literal> and <literal>initrd</literal> files and
-    have it provide the same kernel command line arguments found in
-    <literal>netboot.ipxe</literal>.
-  </para>
-  <para>
-    If you’re using iPXE, depending on how your HTTP/FTP/etc. server is
-    configured you may be able to use <literal>netboot.ipxe</literal>
-    unmodified, or you may need to update the paths to the files to
-    match your server’s directory layout.
-  </para>
-  <para>
-    In the future we may begin making these files available as build
-    products from hydra at which point we will update this documentation
-    with instructions on how to obtain them either for placing on a
-    dedicated TFTP server or to boot them directly over the internet.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/installation/installing-usb.section.xml b/nixos/doc/manual/from_md/installation/installing-usb.section.xml
deleted file mode 100644
index 9d12ac45aac..00000000000
--- a/nixos/doc/manual/from_md/installation/installing-usb.section.xml
+++ /dev/null
@@ -1,135 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-booting-from-usb">
-  <title>Booting from a USB flash drive</title>
-  <para>
-    The image has to be written verbatim to the USB flash drive for it
-    to be bootable on UEFI and BIOS systems. Here are the recommended
-    tools to do that.
-  </para>
-  <section xml:id="sec-booting-from-usb-graphical">
-    <title>Creating bootable USB flash drive with a graphical
-    tool</title>
-    <para>
-      Etcher is a popular and user-friendly tool. It works on Linux,
-      Windows and macOS.
-    </para>
-    <para>
-      Download it from
-      <link xlink:href="https://www.balena.io/etcher/">balena.io</link>,
-      start the program, select the downloaded NixOS ISO, then select
-      the USB flash drive and flash it.
-    </para>
-    <warning>
-      <para>
-        Etcher reports errors and usage statistics by default, which can
-        be disabled in the settings.
-      </para>
-    </warning>
-    <para>
-      An alternative is
-      <link xlink:href="https://bztsrc.gitlab.io/usbimager">USBImager</link>,
-      which is very simple and does not connect to the internet.
-      Download the version with write-only (wo) interface for your
-      system. Start the program, select the image, select the USB flash
-      drive and click <quote>Write</quote>.
-    </para>
-  </section>
-  <section xml:id="sec-booting-from-usb-linux">
-    <title>Creating bootable USB flash drive from a Terminal on
-    Linux</title>
-    <orderedlist numeration="arabic" spacing="compact">
-      <listitem>
-        <para>
-          Plug in the USB flash drive.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Find the corresponding device with <literal>lsblk</literal>.
-          You can distinguish them by their size.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Make sure all partitions on the device are properly unmounted.
-          Replace <literal>sdX</literal> with your device (e.g.
-          <literal>sdb</literal>).
-        </para>
-      </listitem>
-    </orderedlist>
-    <programlisting>
-sudo umount /dev/sdX*
-</programlisting>
-    <orderedlist numeration="arabic" spacing="compact">
-      <listitem override="4">
-        <para>
-          Then use the <literal>dd</literal> utility to write the image
-          to the USB flash drive.
-        </para>
-      </listitem>
-    </orderedlist>
-    <programlisting>
-sudo dd if=&lt;path-to-image&gt; of=/dev/sdX bs=4M conv=fsync
-</programlisting>
-  </section>
-  <section xml:id="sec-booting-from-usb-macos">
-    <title>Creating bootable USB flash drive from a Terminal on
-    macOS</title>
-    <orderedlist numeration="arabic" spacing="compact">
-      <listitem>
-        <para>
-          Plug in the USB flash drive.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Find the corresponding device with
-          <literal>diskutil list</literal>. You can distinguish them by
-          their size.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Make sure all partitions on the device are properly unmounted.
-          Replace <literal>diskX</literal> with your device (e.g.
-          <literal>disk1</literal>).
-        </para>
-      </listitem>
-    </orderedlist>
-    <programlisting>
-diskutil unmountDisk diskX
-</programlisting>
-    <orderedlist numeration="arabic" spacing="compact">
-      <listitem override="4">
-        <para>
-          Then use the <literal>dd</literal> utility to write the image
-          to the USB flash drive.
-        </para>
-      </listitem>
-    </orderedlist>
-    <programlisting>
-sudo dd if=&lt;path-to-image&gt; of=/dev/rdiskX bs=4m
-</programlisting>
-    <para>
-      After <literal>dd</literal> completes, a GUI dialog &quot;The disk
-      you inserted was not readable by this computer&quot; will pop up,
-      which can be ignored.
-    </para>
-    <note>
-      <para>
-        Using the 'raw' <literal>rdiskX</literal> device instead of
-        <literal>diskX</literal> with dd completes in minutes instead of
-        hours.
-      </para>
-    </note>
-    <orderedlist numeration="arabic" spacing="compact">
-      <listitem override="5">
-        <para>
-          Eject the disk when it is finished.
-        </para>
-      </listitem>
-    </orderedlist>
-    <programlisting>
-diskutil eject /dev/diskX
-</programlisting>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/installation/installing-virtualbox-guest.section.xml b/nixos/doc/manual/from_md/installation/installing-virtualbox-guest.section.xml
deleted file mode 100644
index 8b82a617e7f..00000000000
--- a/nixos/doc/manual/from_md/installation/installing-virtualbox-guest.section.xml
+++ /dev/null
@@ -1,92 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-installing-virtualbox-guest">
-  <title>Installing in a VirtualBox guest</title>
-  <para>
-    Installing NixOS into a VirtualBox guest is convenient for users who
-    want to try NixOS without installing it on bare metal. If you want
-    to use a pre-made VirtualBox appliance, it is available at
-    <link xlink:href="https://nixos.org/nixos/download.html">the
-    downloads page</link>. If you want to set up a VirtualBox guest
-    manually, follow these instructions:
-  </para>
-  <orderedlist numeration="arabic">
-    <listitem>
-      <para>
-        Add a New Machine in VirtualBox with OS Type &quot;Linux / Other
-        Linux&quot;
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Base Memory Size: 768 MB or higher.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        New Hard Disk of 8 GB or higher.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Mount the CD-ROM with the NixOS ISO (by clicking on CD/DVD-ROM)
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Click on Settings / System / Processor and enable PAE/NX
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Click on Settings / System / Acceleration and enable
-        &quot;VT-x/AMD-V&quot; acceleration
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Click on Settings / Display / Screen and select VMSVGA as
-        Graphics Controller
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Save the settings, start the virtual machine, and continue
-        installation like normal
-      </para>
-    </listitem>
-  </orderedlist>
-  <para>
-    There are a few modifications you should make in configuration.nix.
-    Enable booting:
-  </para>
-  <programlisting language="bash">
-boot.loader.grub.device = &quot;/dev/sda&quot;;
-</programlisting>
-  <para>
-    Also remove the fsck that runs at startup. It will always fail to
-    run, stopping your boot until you press <literal>*</literal>.
-  </para>
-  <programlisting language="bash">
-boot.initrd.checkJournalingFS = false;
-</programlisting>
-  <para>
-    Shared folders can be given a name and a path in the host system in
-    the VirtualBox settings (Machine / Settings / Shared Folders, then
-    click on the &quot;Add&quot; icon). Add the following to the
-    <literal>/etc/nixos/configuration.nix</literal> to auto-mount them.
-    If you do not add <literal>&quot;nofail&quot;</literal>, the system
-    will not boot properly.
-  </para>
-  <programlisting language="bash">
-{ config, pkgs, ...} :
-{
-  fileSystems.&quot;/virtualboxshare&quot; = {
-    fsType = &quot;vboxsf&quot;;
-    device = &quot;nameofthesharedfolder&quot;;
-    options = [ &quot;rw&quot; &quot;nofail&quot; ];
-  };
-}
-</programlisting>
-  <para>
-    The folder will be available directly under the root directory.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/installation/installing.chapter.xml b/nixos/doc/manual/from_md/installation/installing.chapter.xml
deleted file mode 100644
index c8d1e26b5e7..00000000000
--- a/nixos/doc/manual/from_md/installation/installing.chapter.xml
+++ /dev/null
@@ -1,887 +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" xml:id="sec-installation">
-  <title>Installing NixOS</title>
-  <section xml:id="sec-installation-booting">
-    <title>Booting from the install medium</title>
-    <para>
-      To begin the installation, you have to boot your computer from the
-      install drive.
-    </para>
-    <orderedlist numeration="arabic">
-      <listitem>
-        <para>
-          Plug in the install drive. Then turn on or restart your
-          computer.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Open the boot menu by pressing the appropriate key, which is
-          usually shown on the display on early boot. Select the USB
-          flash drive (the option usually contains the word
-          <quote>USB</quote>). If you choose the incorrect drive, your
-          computer will likely continue to boot as normal. In that case
-          restart your computer and pick a different drive.
-        </para>
-        <note>
-          <para>
-            The key to open the boot menu is different across computer
-            brands and even models. It can be <keycap>F12</keycap>, but
-            also <keycap>F1</keycap>, <keycap>F9</keycap>,
-            <keycap>F10</keycap>, <keycap>Enter</keycap>,
-            <keycap>Del</keycap>, <keycap>Esc</keycap> or another
-            function key. If you are unsure and don’t see it on the
-            early boot screen, you can search online for your computers
-            brand, model followed by <quote>boot from usb</quote>. The
-            computer might not even have that feature, so you have to go
-            into the BIOS/UEFI settings to change the boot order. Again,
-            search online for details about your specific computer
-            model.
-          </para>
-          <para>
-            For Apple computers with Intel processors press and hold the
-            <keycap>⌥</keycap> (Option or Alt) key until you see the
-            boot menu. On Apple silicon press and hold the power button.
-          </para>
-        </note>
-        <note>
-          <para>
-            If your computer supports both BIOS and UEFI boot, choose
-            the UEFI option.
-          </para>
-        </note>
-        <note>
-          <para>
-            If you use a CD for the installation, the computer will
-            probably boot from it automatically. If not, choose the
-            option containing the word <quote>CD</quote> from the boot
-            menu.
-          </para>
-        </note>
-      </listitem>
-      <listitem>
-        <para>
-          Shortly after selecting the appropriate boot drive, you should
-          be presented with a menu with different installer options.
-          Leave the default and wait (or press <keycap>Enter</keycap> to
-          speed up).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The graphical images will start their corresponding desktop
-          environment and the graphical installer, which can take some
-          time. The minimal images will boot to a command line. You have
-          to follow the instructions in
-          <xref linkend="sec-installation-manual" /> there.
-        </para>
-      </listitem>
-    </orderedlist>
-  </section>
-  <section xml:id="sec-installation-graphical">
-    <title>Graphical Installation</title>
-    <para>
-      The graphical installer is recommended for desktop users and will
-      guide you through the installation.
-    </para>
-    <orderedlist numeration="arabic">
-      <listitem>
-        <para>
-          In the <quote>Welcome</quote> screen, you can select the
-          language of the Installer and the installed system.
-        </para>
-        <tip>
-          <para>
-            Leaving the language as <quote>American English</quote> will
-            make it easier to search for error messages in a search
-            engine or to report an issue.
-          </para>
-        </tip>
-      </listitem>
-      <listitem>
-        <para>
-          Next you should choose your location to have the timezone set
-          correctly. You can actually click on the map!
-        </para>
-        <note>
-          <para>
-            The installer will use an online service to guess your
-            location based on your public IP address.
-          </para>
-        </note>
-      </listitem>
-      <listitem>
-        <para>
-          Then you can select the keyboard layout. The default keyboard
-          model should work well with most desktop keyboards. If you
-          have a special keyboard or notebook, your model might be in
-          the list. Select the language you are most comfortable typing
-          in.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          On the <quote>Users</quote> screen, you have to type in your
-          display name, login name and password. You can also enable an
-          option to automatically login to the desktop.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Then you have the option to choose a desktop environment. If
-          you want to create a custom setup with a window manager, you
-          can select <quote>No desktop</quote>.
-        </para>
-        <tip>
-          <para>
-            If you don’t have a favorite desktop and don’t know which
-            one to choose, you can stick to either GNOME or Plasma. They
-            have a quite different design, so you should choose
-            whichever you like better. They are both popular choices and
-            well tested on NixOS.
-          </para>
-        </tip>
-      </listitem>
-      <listitem>
-        <para>
-          You have the option to allow unfree software in the next
-          screen.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The easiest option in the <quote>Partitioning</quote> screen
-          is <quote>Erase disk</quote>, which will delete all data from
-          the selected disk and install the system on it. Also select
-          <quote>Swap (with Hibernation)</quote> in the dropdown below
-          it. You have the option to encrypt the whole disk with LUKS.
-        </para>
-        <note>
-          <para>
-            At the top left you see if the Installer was booted with
-            BIOS or UEFI. If you know your system supports UEFI and it
-            shows <quote>BIOS</quote>, reboot with the correct option.
-          </para>
-        </note>
-        <warning>
-          <para>
-            Make sure you have selected the correct disk at the top and
-            that no valuable data is still on the disk! It will be
-            deleted when formatting the disk.
-          </para>
-        </warning>
-      </listitem>
-      <listitem>
-        <para>
-          Check the choices you made in the <quote>Summary</quote> and
-          click <quote>Install</quote>.
-        </para>
-        <note>
-          <para>
-            The installation takes about 15 minutes. The time varies
-            based on the selected desktop environment, internet
-            connection speed and disk write speed.
-          </para>
-        </note>
-      </listitem>
-      <listitem>
-        <para>
-          When the install is complete, remove the USB flash drive and
-          reboot into your new system!
-        </para>
-      </listitem>
-    </orderedlist>
-  </section>
-  <section xml:id="sec-installation-manual">
-    <title>Manual Installation</title>
-    <para>
-      NixOS can be installed on BIOS or UEFI systems. The procedure for
-      a UEFI installation is broadly the same as for a BIOS
-      installation. The differences are mentioned in the following
-      steps.
-    </para>
-    <para>
-      The NixOS manual is available by running
-      <literal>nixos-help</literal> in the command line or from the
-      application menu in the desktop environment.
-    </para>
-    <para>
-      To have access to the command line on the graphical images, open
-      Terminal (GNOME) or Konsole (Plasma) from the application menu.
-    </para>
-    <para>
-      You are logged-in automatically as <literal>nixos</literal>. The
-      <literal>nixos</literal> user account has an empty password so you
-      can use <literal>sudo</literal> without a password:
-    </para>
-    <programlisting>
-$ sudo -i
-</programlisting>
-    <para>
-      You can use <literal>loadkeys</literal> to switch to your
-      preferred keyboard layout. (We even provide neo2 via
-      <literal>loadkeys de neo</literal>!)
-    </para>
-    <para>
-      If the text is too small to be legible, try
-      <literal>setfont ter-v32n</literal> to increase the font size.
-    </para>
-    <para>
-      To install over a serial port connect with
-      <literal>115200n8</literal> (e.g.
-      <literal>picocom -b 115200 /dev/ttyUSB0</literal>). When the
-      bootloader lists boot entries, select the serial console boot
-      entry.
-    </para>
-    <section xml:id="sec-installation-manual-networking">
-      <title>Networking in the installer</title>
-      <para>
-        <anchor xml:id="sec-installation-booting-networking" />
-        <!-- legacy anchor -->
-      </para>
-      <para>
-        The boot process should have brought up networking (check
-        <literal>ip a</literal>). Networking is necessary for the
-        installer, since it will download lots of stuff (such as source
-        tarballs or Nixpkgs channel binaries). It’s best if you have a
-        DHCP server on your network. Otherwise configure networking
-        manually using <literal>ifconfig</literal>.
-      </para>
-      <para>
-        On the graphical installer, you can configure the network, wifi
-        included, through NetworkManager. Using the
-        <literal>nmtui</literal> program, you can do so even in a
-        non-graphical session. If you prefer to configure the network
-        manually, disable NetworkManager with
-        <literal>systemctl stop NetworkManager</literal>.
-      </para>
-      <para>
-        On the minimal installer, NetworkManager is not available, so
-        configuration must be performed manually. To configure the wifi,
-        first start wpa_supplicant with
-        <literal>sudo systemctl start wpa_supplicant</literal>, then run
-        <literal>wpa_cli</literal>. For most home networks, you need to
-        type in the following commands:
-      </para>
-      <programlisting>
-&gt; add_network
-0
-&gt; set_network 0 ssid &quot;myhomenetwork&quot;
-OK
-&gt; set_network 0 psk &quot;mypassword&quot;
-OK
-&gt; set_network 0 key_mgmt WPA-PSK
-OK
-&gt; enable_network 0
-OK
-</programlisting>
-      <para>
-        For enterprise networks, for example
-        <emphasis>eduroam</emphasis>, instead do:
-      </para>
-      <programlisting>
-&gt; add_network
-0
-&gt; set_network 0 ssid &quot;eduroam&quot;
-OK
-&gt; set_network 0 identity &quot;myname@example.com&quot;
-OK
-&gt; set_network 0 password &quot;mypassword&quot;
-OK
-&gt; set_network 0 key_mgmt WPA-EAP
-OK
-&gt; enable_network 0
-OK
-</programlisting>
-      <para>
-        When successfully connected, you should see a line such as this
-        one
-      </para>
-      <programlisting>
-&lt;3&gt;CTRL-EVENT-CONNECTED - Connection to 32:85:ab:ef:24:5c completed [id=0 id_str=]
-</programlisting>
-      <para>
-        you can now leave <literal>wpa_cli</literal> by typing
-        <literal>quit</literal>.
-      </para>
-      <para>
-        If you would like to continue the installation from a different
-        machine you can use activated SSH daemon. You need to copy your
-        ssh key to either
-        <literal>/home/nixos/.ssh/authorized_keys</literal> or
-        <literal>/root/.ssh/authorized_keys</literal> (Tip: For
-        installers with a modifiable filesystem such as the sd-card
-        installer image a key can be manually placed by mounting the
-        image on a different machine). Alternatively you must set a
-        password for either <literal>root</literal> or
-        <literal>nixos</literal> with <literal>passwd</literal> to be
-        able to login.
-      </para>
-    </section>
-    <section xml:id="sec-installation-manual-partitioning">
-      <title>Partitioning and formatting</title>
-      <para>
-        <anchor xml:id="sec-installation-partitioning" />
-        <!-- legacy anchor -->
-      </para>
-      <para>
-        The NixOS installer doesn’t do any partitioning or formatting,
-        so you need to do that yourself.
-      </para>
-      <para>
-        The NixOS installer ships with multiple partitioning tools. The
-        examples below use <literal>parted</literal>, but also provides
-        <literal>fdisk</literal>, <literal>gdisk</literal>,
-        <literal>cfdisk</literal>, and <literal>cgdisk</literal>.
-      </para>
-      <para>
-        The recommended partition scheme differs depending if the
-        computer uses <emphasis>Legacy Boot</emphasis> or
-        <emphasis>UEFI</emphasis>.
-      </para>
-      <section xml:id="sec-installation-manual-partitioning-UEFI">
-        <title>UEFI (GPT)</title>
-        <para>
-          <anchor xml:id="sec-installation-partitioning-UEFI" />
-          <!-- legacy anchor -->
-        </para>
-        <para>
-          Here's an example partition scheme for UEFI, using
-          <literal>/dev/sda</literal> as the device.
-        </para>
-        <note>
-          <para>
-            You can safely ignore <literal>parted</literal>'s
-            informational message about needing to update /etc/fstab.
-          </para>
-        </note>
-        <orderedlist numeration="arabic">
-          <listitem>
-            <para>
-              Create a <emphasis>GPT</emphasis> partition table.
-            </para>
-            <programlisting>
-# parted /dev/sda -- mklabel gpt
-</programlisting>
-          </listitem>
-          <listitem>
-            <para>
-              Add the <emphasis>root</emphasis> partition. This will
-              fill the disk except for the end part, where the swap will
-              live, and the space left in front (512MiB) which will be
-              used by the boot partition.
-            </para>
-            <programlisting>
-# parted /dev/sda -- mkpart primary 512MB -8GB
-</programlisting>
-          </listitem>
-          <listitem>
-            <para>
-              Next, add a <emphasis>swap</emphasis> partition. The size
-              required will vary according to needs, here a 8GB one is
-              created.
-            </para>
-            <programlisting>
-# parted /dev/sda -- mkpart primary linux-swap -8GB 100%
-</programlisting>
-            <note>
-              <para>
-                The swap partition size rules are no different than for
-                other Linux distributions.
-              </para>
-            </note>
-          </listitem>
-          <listitem>
-            <para>
-              Finally, the <emphasis>boot</emphasis> partition. NixOS by
-              default uses the ESP (EFI system partition) as its
-              <emphasis>/boot</emphasis> partition. It uses the
-              initially reserved 512MiB at the start of the disk.
-            </para>
-            <programlisting>
-# parted /dev/sda -- mkpart ESP fat32 1MB 512MB
-# parted /dev/sda -- set 3 esp on
-</programlisting>
-          </listitem>
-        </orderedlist>
-        <para>
-          Once complete, you can follow with
-          <xref linkend="sec-installation-manual-partitioning-formatting" />.
-        </para>
-      </section>
-      <section xml:id="sec-installation-manual-partitioning-MBR">
-        <title>Legacy Boot (MBR)</title>
-        <para>
-          <anchor xml:id="sec-installation-partitioning-MBR" />
-          <!-- legacy anchor -->
-        </para>
-        <para>
-          Here's an example partition scheme for Legacy Boot, using
-          <literal>/dev/sda</literal> as the device.
-        </para>
-        <note>
-          <para>
-            You can safely ignore <literal>parted</literal>'s
-            informational message about needing to update /etc/fstab.
-          </para>
-        </note>
-        <orderedlist numeration="arabic">
-          <listitem>
-            <para>
-              Create a <emphasis>MBR</emphasis> partition table.
-            </para>
-            <programlisting>
-# parted /dev/sda -- mklabel msdos
-</programlisting>
-          </listitem>
-          <listitem>
-            <para>
-              Add the <emphasis>root</emphasis> partition. This will
-              fill the the disk except for the end part, where the swap
-              will live.
-            </para>
-            <programlisting>
-# parted /dev/sda -- mkpart primary 1MB -8GB
-</programlisting>
-          </listitem>
-          <listitem>
-            <para>
-              Set the root partition’s boot flag to on. This allows the
-              disk to be booted from.
-            </para>
-            <programlisting>
-# parted /dev/sda -- set 1 boot on
-</programlisting>
-          </listitem>
-          <listitem>
-            <para>
-              Finally, add a <emphasis>swap</emphasis> partition. The
-              size required will vary according to needs, here a 8GB one
-              is created.
-            </para>
-            <programlisting>
-# parted /dev/sda -- mkpart primary linux-swap -8GB 100%
-</programlisting>
-            <note>
-              <para>
-                The swap partition size rules are no different than for
-                other Linux distributions.
-              </para>
-            </note>
-          </listitem>
-        </orderedlist>
-        <para>
-          Once complete, you can follow with
-          <xref linkend="sec-installation-manual-partitioning-formatting" />.
-        </para>
-      </section>
-      <section xml:id="sec-installation-manual-partitioning-formatting">
-        <title>Formatting</title>
-        <para>
-          <anchor xml:id="sec-installation-partitioning-formatting" />
-          <!-- legacy anchor -->
-        </para>
-        <para>
-          Use the following commands:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              For initialising Ext4 partitions:
-              <literal>mkfs.ext4</literal>. It is recommended that you
-              assign a unique symbolic label to the file system using
-              the option <literal>-L label</literal>, since this makes
-              the file system configuration independent from device
-              changes. For example:
-            </para>
-            <programlisting>
-# mkfs.ext4 -L nixos /dev/sda1
-</programlisting>
-          </listitem>
-          <listitem>
-            <para>
-              For creating swap partitions: <literal>mkswap</literal>.
-              Again it’s recommended to assign a label to the swap
-              partition: <literal>-L label</literal>. For example:
-            </para>
-            <programlisting>
-# mkswap -L swap /dev/sda2
-</programlisting>
-          </listitem>
-          <listitem>
-            <para>
-              <emphasis role="strong">UEFI systems</emphasis>
-            </para>
-            <para>
-              For creating boot partitions: <literal>mkfs.fat</literal>.
-              Again it’s recommended to assign a label to the boot
-              partition: <literal>-n label</literal>. For example:
-            </para>
-            <programlisting>
-# mkfs.fat -F 32 -n boot /dev/sda3
-</programlisting>
-          </listitem>
-          <listitem>
-            <para>
-              For creating LVM volumes, the LVM commands, e.g.,
-              <literal>pvcreate</literal>, <literal>vgcreate</literal>,
-              and <literal>lvcreate</literal>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              For creating software RAID devices, use
-              <literal>mdadm</literal>.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </section>
-    </section>
-    <section xml:id="sec-installation-manual-installing">
-      <title>Installing</title>
-      <para>
-        <anchor xml:id="sec-installation-installing" />
-        <!-- legacy anchor -->
-      </para>
-      <orderedlist numeration="arabic">
-        <listitem>
-          <para>
-            Mount the target file system on which NixOS should be
-            installed on <literal>/mnt</literal>, e.g.
-          </para>
-          <programlisting>
-# mount /dev/disk/by-label/nixos /mnt
-</programlisting>
-        </listitem>
-        <listitem>
-          <para>
-            <emphasis role="strong">UEFI systems</emphasis>
-          </para>
-          <para>
-            Mount the boot file system on <literal>/mnt/boot</literal>,
-            e.g.
-          </para>
-          <programlisting>
-# mkdir -p /mnt/boot
-# mount /dev/disk/by-label/boot /mnt/boot
-</programlisting>
-        </listitem>
-        <listitem>
-          <para>
-            If your machine has a limited amount of memory, you may want
-            to activate swap devices now
-            (<literal>swapon device</literal>). The installer (or
-            rather, the build actions that it may spawn) may need quite
-            a bit of RAM, depending on your configuration.
-          </para>
-          <programlisting>
-# swapon /dev/sda2
-</programlisting>
-        </listitem>
-        <listitem>
-          <para>
-            You now need to create a file
-            <literal>/mnt/etc/nixos/configuration.nix</literal> that
-            specifies the intended configuration of the system. This is
-            because NixOS has a <emphasis>declarative</emphasis>
-            configuration model: you create or edit a description of the
-            desired configuration of your system, and then NixOS takes
-            care of making it happen. The syntax of the NixOS
-            configuration file is described in
-            <xref linkend="sec-configuration-syntax" />, while a list of
-            available configuration options appears in
-            <xref linkend="ch-options" />. A minimal example is shown in
-            <link linkend="ex-config">Example: NixOS
-            Configuration</link>.
-          </para>
-          <para>
-            The command <literal>nixos-generate-config</literal> can
-            generate an initial configuration file for you:
-          </para>
-          <programlisting>
-# nixos-generate-config --root /mnt
-</programlisting>
-          <para>
-            You should then edit
-            <literal>/mnt/etc/nixos/configuration.nix</literal> to suit
-            your needs:
-          </para>
-          <programlisting>
-# nano /mnt/etc/nixos/configuration.nix
-</programlisting>
-          <para>
-            If you’re using the graphical ISO image, other editors may
-            be available (such as <literal>vim</literal>). If you have
-            network access, you can also install other editors – for
-            instance, you can install Emacs by running
-            <literal>nix-env -f '&lt;nixpkgs&gt;' -iA emacs</literal>.
-          </para>
-          <variablelist>
-            <varlistentry>
-              <term>
-                BIOS systems
-              </term>
-              <listitem>
-                <para>
-                  You <emphasis>must</emphasis> set the option
-                  <xref linkend="opt-boot.loader.grub.device" /> to
-                  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>
-              <term>
-                UEFI systems
-              </term>
-              <listitem>
-                <para>
-                  You must select a boot-loader, either system-boot or
-                  GRUB. The recommended option is systemd-boot: set the
-                  option
-                  <xref linkend="opt-boot.loader.systemd-boot.enable" />
-                  to <literal>true</literal>.
-                  <literal>nixos-generate-config</literal> should do
-                  this automatically for new configurations when booted
-                  in UEFI mode.
-                </para>
-                <para>
-                  You may want to look at the options starting with
-                  <link linkend="opt-boot.loader.efi.canTouchEfiVariables"><literal>boot.loader.efi</literal></link>
-                  and
-                  <link linkend="opt-boot.loader.systemd-boot.enable"><literal>boot.loader.systemd-boot</literal></link>
-                  as well.
-                </para>
-                <para>
-                  If you want to use GRUB, set
-                  <xref linkend="opt-boot.loader.grub.device" /> to
-                  <literal>nodev</literal> and
-                  <xref linkend="opt-boot.loader.grub.efiSupport" /> to
-                  <literal>true</literal>.
-                </para>
-                <para>
-                  With system-boot, you should not need any special
-                  configuration to detect other installed systems. With
-                  GRUB, set
-                  <xref linkend="opt-boot.loader.grub.useOSProber" /> to
-                  <literal>true</literal>, but this will only detect
-                  windows partitions, not other linux distributions. If
-                  you dual boot another linux distribution, use
-                  system-boot instead.
-                </para>
-              </listitem>
-            </varlistentry>
-          </variablelist>
-          <para>
-            If you need to configure networking for your machine the
-            configuration options are described in
-            <xref linkend="sec-networking" />. In particular, while wifi
-            is supported on the installation image, it is not enabled by
-            default in the configuration generated by
-            <literal>nixos-generate-config</literal>.
-          </para>
-          <para>
-            Another critical option is <literal>fileSystems</literal>,
-            specifying the file systems that need to be mounted by
-            NixOS. However, you typically don’t need to set it yourself,
-            because <literal>nixos-generate-config</literal> sets it
-            automatically in
-            <literal>/mnt/etc/nixos/hardware-configuration.nix</literal>
-            from your currently mounted file systems. (The configuration
-            file <literal>hardware-configuration.nix</literal> is
-            included from <literal>configuration.nix</literal> and will
-            be overwritten by future invocations of
-            <literal>nixos-generate-config</literal>; thus, you
-            generally should not modify it.) Additionally, you may want
-            to look at
-            <link xlink:href="https://github.com/NixOS/nixos-hardware">Hardware
-            configuration for known-hardware</link> at this point or
-            after installation.
-          </para>
-          <note>
-            <para>
-              Depending on your hardware configuration or type of file
-              system, you may need to set the option
-              <literal>boot.initrd.kernelModules</literal> to include
-              the kernel modules that are necessary for mounting the
-              root file system, otherwise the installed system will not
-              be able to boot. (If this happens, boot from the
-              installation media again, mount the target file system on
-              <literal>/mnt</literal>, fix
-              <literal>/mnt/etc/nixos/configuration.nix</literal> and
-              rerun <literal>nixos-install</literal>.) In most cases,
-              <literal>nixos-generate-config</literal> will figure out
-              the required modules.
-            </para>
-          </note>
-        </listitem>
-        <listitem>
-          <para>
-            Do the installation:
-          </para>
-          <programlisting>
-# nixos-install
-</programlisting>
-          <para>
-            This will install your system based on the configuration you
-            provided. If anything fails due to a configuration problem
-            or any other issue (such as a network outage while
-            downloading binaries from the NixOS binary cache), you can
-            re-run <literal>nixos-install</literal> after fixing your
-            <literal>configuration.nix</literal>.
-          </para>
-          <para>
-            As the last step, <literal>nixos-install</literal> will ask
-            you to set the password for the <literal>root</literal>
-            user, e.g.
-          </para>
-          <programlisting>
-setting root password...
-New password: ***
-Retype new password: ***
-</programlisting>
-          <note>
-            <para>
-              For unattended installations, it is possible to use
-              <literal>nixos-install --no-root-passwd</literal> in order
-              to disable the password prompt entirely.
-            </para>
-          </note>
-        </listitem>
-        <listitem>
-          <para>
-            If everything went well:
-          </para>
-          <programlisting>
-# reboot
-</programlisting>
-        </listitem>
-        <listitem>
-          <para>
-            You should now be able to boot into the installed NixOS. The
-            GRUB boot menu shows a list of <emphasis>available
-            configurations</emphasis> (initially just one). Every time
-            you change the NixOS configuration (see
-            <link linkend="sec-changing-config">Changing
-            Configuration</link>), a new item is added to the menu. This
-            allows you to easily roll back to a previous configuration
-            if something goes wrong.
-          </para>
-          <para>
-            You should log in and change the <literal>root</literal>
-            password with <literal>passwd</literal>.
-          </para>
-          <para>
-            You’ll probably want to create some user accounts as well,
-            which can be done with <literal>useradd</literal>:
-          </para>
-          <programlisting>
-$ useradd -c 'Eelco Dolstra' -m eelco
-$ passwd eelco
-</programlisting>
-          <para>
-            You may also want to install some software. This will be
-            covered in <xref linkend="sec-package-management" />.
-          </para>
-        </listitem>
-      </orderedlist>
-    </section>
-    <section xml:id="sec-installation-manual-summary">
-      <title>Installation summary</title>
-      <para>
-        <anchor xml:id="sec-installation-summary" />
-        <!-- legacy anchor -->
-      </para>
-      <para>
-        To summarise, <link linkend="ex-install-sequence">Example:
-        Commands for Installing NixOS on
-        <literal>/dev/sda</literal></link> shows a typical sequence of
-        commands for installing NixOS on an empty hard drive (here
-        <literal>/dev/sda</literal>). <link linkend="ex-config">Example:
-        NixOS Configuration</link> shows a corresponding configuration
-        Nix expression.
-      </para>
-      <anchor xml:id="ex-partition-scheme-MBR" />
-      <para>
-        <emphasis role="strong">Example: Example partition schemes for
-        NixOS on <literal>/dev/sda</literal> (MBR)</emphasis>
-      </para>
-      <programlisting>
-# parted /dev/sda -- mklabel msdos
-# parted /dev/sda -- mkpart primary 1MB -8GB
-# parted /dev/sda -- mkpart primary linux-swap -8GB 100%
-</programlisting>
-      <anchor xml:id="ex-partition-scheme-UEFI" />
-      <para>
-        <emphasis role="strong">Example: Example partition schemes for
-        NixOS on <literal>/dev/sda</literal> (UEFI)</emphasis>
-      </para>
-      <programlisting>
-# parted /dev/sda -- mklabel gpt
-# parted /dev/sda -- mkpart primary 512MB -8GB
-# parted /dev/sda -- mkpart primary linux-swap -8GB 100%
-# parted /dev/sda -- mkpart ESP fat32 1MB 512MB
-# parted /dev/sda -- set 3 esp on
-</programlisting>
-      <anchor xml:id="ex-install-sequence" />
-      <para>
-        <emphasis role="strong">Example: Commands for Installing NixOS
-        on <literal>/dev/sda</literal></emphasis>
-      </para>
-      <para>
-        With a partitioned disk.
-      </para>
-      <programlisting>
-# mkfs.ext4 -L nixos /dev/sda1
-# mkswap -L swap /dev/sda2
-# swapon /dev/sda2
-# mkfs.fat -F 32 -n boot /dev/sda3        # (for UEFI systems only)
-# mount /dev/disk/by-label/nixos /mnt
-# mkdir -p /mnt/boot                      # (for UEFI systems only)
-# mount /dev/disk/by-label/boot /mnt/boot # (for UEFI systems only)
-# nixos-generate-config --root /mnt
-# nano /mnt/etc/nixos/configuration.nix
-# nixos-install
-# reboot
-</programlisting>
-      <anchor xml:id="ex-config" />
-      <para>
-        <emphasis role="strong">Example: NixOS Configuration</emphasis>
-      </para>
-      <programlisting>
-{ config, pkgs, ... }: {
-  imports = [
-    # Include the results of the hardware scan.
-    ./hardware-configuration.nix
-  ];
-
-  boot.loader.grub.device = &quot;/dev/sda&quot;;   # (for BIOS systems only)
-  boot.loader.systemd-boot.enable = true; # (for UEFI systems only)
-
-  # Note: setting fileSystems is generally not
-  # necessary, since nixos-generate-config figures them out
-  # automatically in hardware-configuration.nix.
-  #fileSystems.&quot;/&quot;.device = &quot;/dev/disk/by-label/nixos&quot;;
-
-  # Enable the OpenSSH server.
-  services.sshd.enable = true;
-}
-</programlisting>
-    </section>
-  </section>
-  <section xml:id="sec-installation-additional-notes">
-    <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" />
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/installation/obtaining.chapter.xml b/nixos/doc/manual/from_md/installation/obtaining.chapter.xml
deleted file mode 100644
index d187adfc0c5..00000000000
--- a/nixos/doc/manual/from_md/installation/obtaining.chapter.xml
+++ /dev/null
@@ -1,47 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-obtaining">
-  <title>Obtaining NixOS</title>
-  <para>
-    NixOS ISO images can be downloaded from the
-    <link xlink:href="https://nixos.org/download.html#nixos-iso">NixOS
-    download page</link>. Follow the instructions in
-    <xref linkend="sec-booting-from-usb" /> to create a bootable USB
-    flash drive.
-  </para>
-  <para>
-    If you have a very old system that can’t boot from USB, you can burn
-    the image to an empty CD. NixOS might not work very well on such
-    systems.
-  </para>
-  <para>
-    As an alternative to installing NixOS yourself, you can get a
-    running NixOS system through several other means:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        Using virtual appliances in Open Virtualization Format (OVF)
-        that can be imported into VirtualBox. These are available from
-        the
-        <link xlink:href="https://nixos.org/download.html#nixos-virtualbox">NixOS
-        download page</link>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Using AMIs for Amazon’s EC2. To find one for your region, please
-        refer to the
-        <link xlink:href="https://nixos.org/download.html#nixos-amazon">download
-        page</link>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Using NixOps, the NixOS-based cloud deployment tool, which
-        allows you to provision VirtualBox and EC2 NixOS instances from
-        declarative specifications. Check out the
-        <link xlink:href="https://nixos.org/nixops">NixOps
-        homepage</link> for details.
-      </para>
-    </listitem>
-  </itemizedlist>
-</chapter>
diff --git a/nixos/doc/manual/from_md/installation/upgrading.chapter.xml b/nixos/doc/manual/from_md/installation/upgrading.chapter.xml
deleted file mode 100644
index f6aedc800ac..00000000000
--- a/nixos/doc/manual/from_md/installation/upgrading.chapter.xml
+++ /dev/null
@@ -1,152 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-upgrading">
-  <title>Upgrading NixOS</title>
-  <para>
-    The best way to keep your NixOS installation up to date is to use
-    one of the NixOS <emphasis>channels</emphasis>. A channel is a Nix
-    mechanism for distributing Nix 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:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        <emphasis>Stable channels</emphasis>, such as
-        <link xlink:href="https://nixos.org/channels/nixos-22.05"><literal>nixos-22.11</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),
-        but not from 4.19.x to 4.20.x (a major change that has the
-        potential to break things). Stable channels are generally
-        maintained until the next stable branch is created.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The <emphasis>unstable channel</emphasis>,
-        <link xlink:href="https://nixos.org/channels/nixos-unstable"><literal>nixos-unstable</literal></link>.
-        This corresponds to NixOS’s main development branch, and may
-        thus see radical changes between channel updates. It’s not
-        recommended for production systems.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <emphasis>Small channels</emphasis>, such as
-        <link xlink:href="https://nixos.org/channels/nixos-22.05-small"><literal>nixos-22.11-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
-        described above, except that they contain fewer binary packages.
-        This means they get updated faster than the regular channels
-        (for instance, when a critical security patch is committed to
-        NixOS’s source tree), but may require more packages to be built
-        from source than usual. They’re mostly intended for server
-        environments and as such contain few GUI applications.
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    To see what channels are available, go to
-    <link xlink:href="https://nixos.org/channels">https://nixos.org/channels</link>.
-    (Note that the URIs of the various channels redirect to a directory
-    that contains the channel’s latest version and includes ISO images
-    and VirtualBox appliances.) Please note that during the release
-    process, channels that are not yet released will be present here as
-    well. See the Getting NixOS page
-    <link xlink:href="https://nixos.org/nixos/download.html">https://nixos.org/nixos/download.html</link>
-    to find the newest supported stable release.
-  </para>
-  <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 22.11 ISO, you will be subscribed
-    to the <literal>nixos-22.11</literal> channel. To see which NixOS
-    channel you’re subscribed to, run the following as root:
-  </para>
-  <programlisting>
-# nix-channel --list | grep nixos
-nixos https://nixos.org/channels/nixos-unstable
-</programlisting>
-  <para>
-    To switch to a different NixOS channel, do
-  </para>
-  <programlisting>
-# nix-channel --add https://nixos.org/channels/channel-name nixos
-</programlisting>
-  <para>
-    (Be sure to include the <literal>nixos</literal> parameter at the
-    end.) For instance, to use the NixOS 22.11 stable channel:
-  </para>
-  <programlisting>
-# nix-channel --add https://nixos.org/channels/nixos-22.11 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-22.11-small nixos
-</programlisting>
-  <para>
-    And if you want to live on the bleeding edge:
-  </para>
-  <programlisting>
-# nix-channel --add https://nixos.org/channels/nixos-unstable nixos
-</programlisting>
-  <para>
-    You can then upgrade NixOS to the latest version in your chosen
-    channel by running
-  </para>
-  <programlisting>
-# nixos-rebuild switch --upgrade
-</programlisting>
-  <para>
-    which is equivalent to the more verbose
-    <literal>nix-channel --update nixos; nixos-rebuild switch</literal>.
-  </para>
-  <note>
-    <para>
-      Channels are set per user. This means that running
-      <literal>nix-channel --add</literal> as a non root user (or
-      without sudo) will not affect configuration in
-      <literal>/etc/nixos/configuration.nix</literal>
-    </para>
-  </note>
-  <warning>
-    <para>
-      It is generally safe to switch back and forth between channels.
-      The only exception is that a newer NixOS may also have a newer Nix
-      version, which may involve an upgrade of Nix’s database schema.
-      This cannot be undone easily, so in that case you will not be able
-      to go back to your original channel.
-    </para>
-  </warning>
-  <section xml:id="sec-upgrading-automatic">
-    <title>Automatic Upgrades</title>
-    <para>
-      You can keep a NixOS system up-to-date automatically by adding the
-      following to <literal>configuration.nix</literal>:
-    </para>
-    <programlisting language="bash">
-system.autoUpgrade.enable = true;
-system.autoUpgrade.allowReboot = true;
-</programlisting>
-    <para>
-      This enables a periodically executed systemd service named
-      <literal>nixos-upgrade.service</literal>. If the
-      <literal>allowReboot</literal> option is <literal>false</literal>,
-      it runs <literal>nixos-rebuild switch --upgrade</literal> to
-      upgrade NixOS to the latest version in the current channel. (To
-      see when the service runs, see
-      <literal>systemctl list-timers</literal>.) If
-      <literal>allowReboot</literal> is <literal>true</literal>, then
-      the system will automatically reboot if the new generation
-      contains a different kernel, initrd or kernel modules. You can
-      also specify a channel explicitly, e.g.
-    </para>
-    <programlisting language="bash">
-system.autoUpgrade.channel = https://nixos.org/channels/nixos-22.11;
-</programlisting>
-  </section>
-</chapter>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1310.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1310.section.xml
deleted file mode 100644
index b4f3657b4b8..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1310.section.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-13.10">
-  <title>Release 13.10 (<quote>Aardvark</quote>, 2013/10/31)</title>
-  <para>
-    This is the first stable release branch of NixOS.
-  </para>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1404.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1404.section.xml
deleted file mode 100644
index 8771623b468..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1404.section.xml
+++ /dev/null
@@ -1,189 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-14.04">
-  <title>Release 14.04 (<quote>Baboon</quote>, 2014/04/30)</title>
-  <para>
-    This is the second stable release branch of NixOS. In addition to
-    numerous new and upgraded packages and modules, this release has the
-    following highlights:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        Installation on UEFI systems is now supported. See
-        <xref linkend="sec-installation" /> for details.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Systemd has been updated to version 212, which has
-        <link xlink:href="http://cgit.freedesktop.org/systemd/systemd/plain/NEWS?id=v212">numerous
-        improvements</link>. NixOS now automatically starts systemd user
-        instances when you log in. You can define global user units
-        through the <literal>systemd.unit.*</literal> options.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        NixOS is now based on Glibc 2.19 and GCC 4.8.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The default Linux kernel has been updated to 3.12.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        KDE has been updated to 4.12.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        GNOME 3.10 experimental support has been added.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Nix has been updated to 1.7
-        (<link xlink:href="https://nixos.org/nix/manual/#ssec-relnotes-1.7">details</link>).
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        NixOS now supports fully declarative management of users and
-        groups. If you set <literal>users.mutableUsers</literal> to
-        <literal>false</literal>, then the contents of
-        <literal>/etc/passwd</literal> and <literal>/etc/group</literal>
-        will be
-        <link xlink:href="https://www.usenix.org/legacy/event/lisa02/tech/full_papers/traugott/traugott_html/">congruent</link>
-        to your NixOS configuration. For instance, if you remove a user
-        from <literal>users.extraUsers</literal> and run
-        <literal>nixos-rebuild</literal>, the user account will cease to
-        exist. Also, imperative commands for managing users and groups,
-        such as <literal>useradd</literal>, are no longer available. If
-        <literal>users.mutableUsers</literal> is <literal>true</literal>
-        (the default), then behaviour is unchanged from NixOS 13.10.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        NixOS now has basic container support, meaning you can easily
-        run a NixOS instance as a container in a NixOS host system.
-        These containers are suitable for testing and experimentation
-        but not production use, since they’re not fully isolated from
-        the host. See <xref linkend="ch-containers" /> for details.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Systemd units provided by packages can now be overridden from
-        the NixOS configuration. For instance, if a package
-        <literal>foo</literal> provides systemd units, you can say:
-      </para>
-      <programlisting language="bash">
-{
-  systemd.packages = [ pkgs.foo ];
-}
-</programlisting>
-      <para>
-        to enable those units. You can then set or override unit options
-        in the usual way, e.g.
-      </para>
-      <programlisting language="bash">
-{
-  systemd.services.foo.wantedBy = [ &quot;multi-user.target&quot; ];
-  systemd.services.foo.serviceConfig.MemoryLimit = &quot;512M&quot;;
-}
-</programlisting>
-      <para>
-        When upgrading from a previous release, please be aware of the
-        following incompatible changes:
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Nixpkgs no longer exposes unfree packages by default. If your
-        NixOS configuration requires unfree packages from Nixpkgs, you
-        need to enable support for them explicitly by setting:
-      </para>
-      <programlisting language="bash">
-{
-  nixpkgs.config.allowUnfree = true;
-}
-</programlisting>
-      <para>
-        Otherwise, you get an error message such as:
-      </para>
-      <programlisting>
-    error: package ‘nvidia-x11-331.49-3.12.17’ in ‘…/nvidia-x11/default.nix:56’
-      has an unfree license, refusing to evaluate
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        The Adobe Flash player is no longer enabled by default in the
-        Firefox and Chromium wrappers. To enable it, you must set:
-      </para>
-      <programlisting language="bash">
-{
-  nixpkgs.config.allowUnfree = true;
-  nixpkgs.config.firefox.enableAdobeFlash = true; # for Firefox
-  nixpkgs.config.chromium.enableAdobeFlash = true; # for Chromium
-}
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        The firewall is now enabled by default. If you don’t want this,
-        you need to disable it explicitly:
-      </para>
-      <programlisting language="bash">
-{
-  networking.firewall.enable = false;
-}
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        The option <literal>boot.loader.grub.memtest86</literal> has
-        been renamed to
-        <literal>boot.loader.grub.memtest86.enable</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The <literal>mysql55</literal> service has been merged into the
-        <literal>mysql</literal> service, which no longer sets a default
-        for the option <literal>services.mysql.package</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Package variants are now differentiated by suffixing the name,
-        rather than the version. For instance,
-        <literal>sqlite-3.8.4.3-interactive</literal> is now called
-        <literal>sqlite-interactive-3.8.4.3</literal>. This ensures that
-        <literal>nix-env -i sqlite</literal> is unambiguous, and that
-        <literal>nix-env -u</literal> won’t <quote>upgrade</quote>
-        <literal>sqlite</literal> to
-        <literal>sqlite-interactive</literal> or vice versa. Notably,
-        this change affects the Firefox wrapper (which provides
-        plugins), as it is now called
-        <literal>firefox-wrapper</literal>. So when using
-        <literal>nix-env</literal>, you should do
-        <literal>nix-env -e firefox; nix-env -i firefox-wrapper</literal>
-        if you want to keep using the wrapper. This change does not
-        affect declarative package management, since attribute names
-        like <literal>pkgs.firefoxWrapper</literal> were already
-        unambiguous.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The symlink <literal>/etc/ca-bundle.crt</literal> is gone.
-        Programs should instead use the environment variable
-        <literal>OPENSSL_X509_CERT_FILE</literal> (which points to
-        <literal>/etc/ssl/certs/ca-bundle.crt</literal>).
-      </para>
-    </listitem>
-  </itemizedlist>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1412.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1412.section.xml
deleted file mode 100644
index 3b6af73359d..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1412.section.xml
+++ /dev/null
@@ -1,466 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-14.12">
-  <title>Release 14.12 (<quote>Caterpillar</quote>, 2014/12/30)</title>
-  <para>
-    In addition to numerous new and upgraded packages, this release has
-    the following highlights:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        Systemd has been updated to version 217, which has numerous
-        <link xlink:href="http://lists.freedesktop.org/archives/systemd-devel/2014-October/024662.html">improvements.</link>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <link xlink:href="https://www.mail-archive.com/nix-dev@lists.science.uu.nl/msg13957.html">Nix
-        has been updated to 1.8.</link>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        NixOS is now based on Glibc 2.20.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        KDE has been updated to 4.14.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The default Linux kernel has been updated to 3.14.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        If <literal>users.mutableUsers</literal> is enabled (the
-        default), changes made to the declaration of a user or group
-        will be correctly realised when running
-        <literal>nixos-rebuild</literal>. For instance, removing a user
-        specification from <literal>configuration.nix</literal> will
-        cause the actual user account to be deleted. If
-        <literal>users.mutableUsers</literal> is disabled, it is no
-        longer necessary to specify UIDs or GIDs; if omitted, they are
-        allocated dynamically.
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    Following new services were added since the last release:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        <literal>atftpd</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>bosun</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>bspwm</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>chronos</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>collectd</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>consul</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>cpuminer-cryptonight</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>crashplan</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>dnscrypt-proxy</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>docker-registry</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>docker</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>etcd</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>fail2ban</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>fcgiwrap</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>fleet</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>fluxbox</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>gdm</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>geoclue2</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>gitlab</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>gitolite</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>gnome3.gnome-documents</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>gnome3.gnome-online-miners</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>gnome3.gvfs</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>gnome3.seahorse</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>hbase</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>i2pd</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>influxdb</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>kubernetes</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>liquidsoap</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>lxc</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>mailpile</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>mesos</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>mlmmj</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>monetdb</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>mopidy</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>neo4j</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>nsd</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>openntpd</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>opentsdb</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>openvswitch</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>parallels-guest</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>peerflix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>phd</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>polipo</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>prosody</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>radicale</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>redmine</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>riemann</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>scollector</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>seeks</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>siproxd</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>strongswan</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>tcsd</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>teamspeak3</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>thermald</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>torque/mrom</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>torque/server</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>uhub</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>unifi</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>znc</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>zookeeper</literal>
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    When upgrading from a previous release, please be aware of the
-    following incompatible changes:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        The default version of Apache httpd is now 2.4. If you use the
-        <literal>extraConfig</literal> option to pass literal Apache
-        configuration text, you may need to update it — see
-        <link xlink:href="http://httpd.apache.org/docs/2.4/upgrading.html">Apache’s
-        documentation</link> for details. If you wish to continue to use
-        httpd 2.2, add the following line to your NixOS configuration:
-      </para>
-      <programlisting language="bash">
-{
-  services.httpd.package = pkgs.apacheHttpd_2_2;
-}
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        PHP 5.3 has been removed because it is no longer supported by
-        the PHP project. A
-        <link xlink:href="http://php.net/migration54">migration
-        guide</link> is available.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The host side of a container virtual Ethernet pair is now called
-        <literal>ve-container-name</literal> rather than
-        <literal>c-container-name</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        GNOME 3.10 support has been dropped. The default GNOME version
-        is now 3.12.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        VirtualBox has been upgraded to 4.3.20 release. Users may be
-        required to run <literal>rm -rf /tmp/.vbox*</literal>. The line
-        <literal>imports = [ &lt;nixpkgs/nixos/modules/programs/virtualbox.nix&gt; ]</literal>
-        is no longer necessary, use
-        <literal>services.virtualboxHost.enable = true</literal>
-        instead.
-      </para>
-      <para>
-        Also, hardening mode is now enabled by default, which means that
-        unless you want to use USB support, you no longer need to be a
-        member of the <literal>vboxusers</literal> group.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Chromium has been updated to 39.0.2171.65.
-        <literal>enablePepperPDF</literal> is now enabled by default.
-        <literal>chromium*Wrapper</literal> packages no longer exist,
-        because upstream removed NSAPI support.
-        <literal>chromium-stable</literal> has been renamed to
-        <literal>chromium</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Python packaging documentation is now part of nixpkgs manual. To
-        override the python packages available to a custom python you
-        now use <literal>pkgs.pythonFull.buildEnv.override</literal>
-        instead of <literal>pkgs.pythonFull.override</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>boot.resumeDevice = &quot;8:6&quot;</literal> is no
-        longer supported. Most users will want to leave it undefined,
-        which takes the swap partitions automatically. There is an
-        evaluation assertion to ensure that the string starts with a
-        slash.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The system-wide default timezone for NixOS installations changed
-        from <literal>CET</literal> to <literal>UTC</literal>. To choose
-        a different timezone for your system, configure
-        <literal>time.timeZone</literal> in
-        <literal>configuration.nix</literal>. A fairly complete list of
-        possible values for that setting is available at
-        <link xlink:href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">https://en.wikipedia.org/wiki/List_of_tz_database_time_zones</link>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        GNU screen has been updated to 4.2.1, which breaks the ability
-        to connect to sessions created by older versions of screen.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The Intel GPU driver was updated to the 3.x prerelease version
-        (used by most distributions) and supports DRI3 now.
-      </para>
-    </listitem>
-  </itemizedlist>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1509.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1509.section.xml
deleted file mode 100644
index 68d2ab389e8..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1509.section.xml
+++ /dev/null
@@ -1,776 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-15.09">
-  <title>Release 15.09 (<quote>Dingo</quote>, 2015/09/30)</title>
-  <para>
-    In addition to numerous new and upgraded packages, this release has
-    the following highlights:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        The <link xlink:href="http://haskell.org/">Haskell</link>
-        packages infrastructure has been re-designed from the ground up
-        (&quot;Haskell NG&quot;). NixOS now distributes the latest
-        version of every single package registered on
-        <link xlink:href="http://hackage.haskell.org/">Hackage</link> --
-        well in excess of 8,000 Haskell packages. Detailed instructions
-        on how to use that infrastructure can be found in the
-        <link xlink:href="https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure">User's
-        Guide to the Haskell Infrastructure</link>. Users migrating from
-        an earlier release may find helpful information below, in the
-        list of backwards-incompatible changes. Furthermore, we
-        distribute 51(!) additional Haskell package sets that provide
-        every single <link xlink:href="http://www.stackage.org/">LTS
-        Haskell</link> release since version 0.0 as well as the most
-        recent <link xlink:href="http://www.stackage.org/">Stackage
-        Nightly</link> snapshot. The announcement
-        <link xlink:href="https://nixos.org/nix-dev/2015-September/018138.html">&quot;Full
-        Stackage Support in Nixpkgs&quot;</link> gives additional
-        details.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Nix has been updated to version 1.10, which among other
-        improvements enables cryptographic signatures on binary caches
-        for improved security.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        You can now keep your NixOS system up to date automatically by
-        setting
-      </para>
-    </listitem>
-  </itemizedlist>
-  <programlisting language="bash">
-{
-  system.autoUpgrade.enable = true;
-}
-</programlisting>
-  <para>
-    This will cause the system to periodically check for updates in your
-    current channel and run <literal>nixos-rebuild</literal>.
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        This release is based on Glibc 2.21, GCC 4.9 and Linux 3.18.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        GNOME has been upgraded to 3.16.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Xfce has been upgraded to 4.12.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        KDE 5 has been upgraded to KDE Frameworks 5.10, Plasma 5.3.2 and
-        Applications 15.04.3. KDE 4 has been updated to kdelibs-4.14.10.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        E19 has been upgraded to 0.16.8.15.
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    The following new services were added since the last release:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        <literal>services/mail/exim.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/apache-kafka.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/canto-daemon.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/confd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/devmon.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/gitit.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/ihaskell.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/mbpfan.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/mediatomb.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/mwlib.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/parsoid.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/plex.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/ripple-rest.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/ripple-data-api.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/subsonic.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/sundtek.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/monitoring/cadvisor.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/monitoring/das_watchdog.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/monitoring/grafana.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/monitoring/riemann-tools.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/monitoring/teamviewer.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/network-filesystems/u9fs.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/aiccu.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/asterisk.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/bird.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/charybdis.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/docker-registry-server.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/fan.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/firefox/sync-server.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/gateone.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/heyefi.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/i2p.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/lambdabot.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/mstpd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/nix-serve.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/nylon.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/racoon.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/skydns.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/shout.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/softether.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/sslh.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/tinc.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/tlsdated.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/tox-bootstrapd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/tvheadend.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/zerotierone.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/scheduling/marathon.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/security/fprintd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/security/hologram.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/security/munge.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/system/cloud-init.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/web-servers/shellinabox.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/web-servers/uwsgi.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/x11/unclutter.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/x11/display-managers/sddm.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/coredump.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/loader/loader.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/loader/generic-extlinux-compatible</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/networkd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/resolved.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/timesyncd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>tasks/filesystems/exfat.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>tasks/filesystems/ntfs.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>tasks/filesystems/vboxsf.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>virtualisation/virtualbox-host.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>virtualisation/vmware-guest.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>virtualisation/xen-dom0.nix</literal>
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    When upgrading from a previous release, please be aware of the
-    following incompatible changes:
-  </para>
-  <itemizedlist spacing="compact">
-    <listitem>
-      <para>
-        <literal>sshd</literal> no longer supports DSA and ECDSA host
-        keys by default. If you have existing systems with such host
-        keys and want to continue to use them, please set
-      </para>
-    </listitem>
-  </itemizedlist>
-  <programlisting language="bash">
-{
-  system.stateVersion = &quot;14.12&quot;;
-}
-</programlisting>
-  <para>
-    The new option <literal>system.stateVersion</literal> ensures that
-    certain configuration changes that could break existing systems
-    (such as the <literal>sshd</literal> host key setting) will maintain
-    compatibility with the specified NixOS release. NixOps sets the
-    state version of existing deployments automatically.
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        <literal>cron</literal> is no longer enabled by default, unless
-        you have a non-empty
-        <literal>services.cron.systemCronJobs</literal>. To force
-        <literal>cron</literal> to be enabled, set
-        <literal>services.cron.enable = true</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Nix now requires binary caches to be cryptographically signed.
-        If you have unsigned binary caches that you want to continue to
-        use, you should set
-        <literal>nix.requireSignedBinaryCaches = false</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Steam now doesn't need root rights to work. Instead of using
-        <literal>*-steam-chrootenv</literal>, you should now just run
-        <literal>steam</literal>. <literal>steamChrootEnv</literal>
-        package was renamed to <literal>steam</literal>, and old
-        <literal>steam</literal> package -- to
-        <literal>steamOriginal</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        CMPlayer has been renamed to bomi upstream. Package
-        <literal>cmplayer</literal> was accordingly renamed to
-        <literal>bomi</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Atom Shell has been renamed to Electron upstream. Package
-        <literal>atom-shell</literal> was accordingly renamed to
-        <literal>electron</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Elm is not released on Hackage anymore. You should now use
-        <literal>elmPackages.elm</literal> which contains the latest Elm
-        platform.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The CUPS printing service has been updated to version
-        <literal>2.0.2</literal>. Furthermore its systemd service has
-        been renamed to <literal>cups.service</literal>.
-      </para>
-      <para>
-        Local printers are no longer shared or advertised by default.
-        This behavior can be changed by enabling
-        <literal>services.printing.defaultShared</literal> or
-        <literal>services.printing.browsing</literal> respectively.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The VirtualBox host and guest options have been named more
-        consistently. They can now found in
-        <literal>virtualisation.virtualbox.host.*</literal> instead of
-        <literal>services.virtualboxHost.*</literal> and
-        <literal>virtualisation.virtualbox.guest.*</literal> instead of
-        <literal>services.virtualboxGuest.*</literal>.
-      </para>
-      <para>
-        Also, there now is support for the <literal>vboxsf</literal>
-        file system using the <literal>fileSystems</literal>
-        configuration attribute. An example of how this can be used in a
-        configuration:
-      </para>
-    </listitem>
-  </itemizedlist>
-  <programlisting language="bash">
-{
-  fileSystems.&quot;/shiny&quot; = {
-    device = &quot;myshinysharedfolder&quot;;
-    fsType = &quot;vboxsf&quot;;
-  };
-}
-</programlisting>
-  <itemizedlist spacing="compact">
-    <listitem>
-      <para>
-        &quot;<literal>nix-env -qa</literal>&quot; no longer discovers
-        Haskell packages by name. The only packages visible in the
-        global scope are <literal>ghc</literal>,
-        <literal>cabal-install</literal>, and <literal>stack</literal>,
-        but all other packages are hidden. The reason for this
-        inconvenience is the sheer size of the Haskell package set.
-        Name-based lookups are expensive, and most
-        <literal>nix-env -qa</literal> operations would become much
-        slower if we'd add the entire Hackage database into the top
-        level attribute set. Instead, the list of Haskell packages can
-        be displayed by running:
-      </para>
-    </listitem>
-  </itemizedlist>
-  <programlisting>
-nix-env -f &quot;&lt;nixpkgs&gt;&quot; -qaP -A haskellPackages
-</programlisting>
-  <para>
-    Executable programs written in Haskell can be installed with:
-  </para>
-  <programlisting>
-nix-env -f &quot;&lt;nixpkgs&gt;&quot; -iA haskellPackages.pandoc
-</programlisting>
-  <para>
-    Installing Haskell <emphasis>libraries</emphasis> this way, however,
-    is no longer supported. See the next item for more details.
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        Previous versions of NixOS came with a feature called
-        <literal>ghc-wrapper</literal>, a small script that allowed GHC
-        to transparently pick up on libraries installed in the user's
-        profile. This feature has been deprecated;
-        <literal>ghc-wrapper</literal> was removed from the
-        distribution. The proper way to register Haskell libraries with
-        the compiler now is the
-        <literal>haskellPackages.ghcWithPackages</literal> function. The
-        <link xlink:href="https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure">User's
-        Guide to the Haskell Infrastructure</link> provides more
-        information about this subject.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        All Haskell builds that have been generated with version 1.x of
-        the <literal>cabal2nix</literal> utility are now invalid and
-        need to be re-generated with a current version of
-        <literal>cabal2nix</literal> to function. The most recent
-        version of this tool can be installed by running
-        <literal>nix-env -i cabal2nix</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The <literal>haskellPackages</literal> set in Nixpkgs used to
-        have a function attribute called <literal>extension</literal>
-        that users could override in their
-        <literal>~/.nixpkgs/config.nix</literal> files to configure
-        additional attributes, etc. That function still exists, but it's
-        now called <literal>overrides</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The OpenBLAS library has been updated to version
-        <literal>0.2.14</literal>. Support for the
-        <literal>x86_64-darwin</literal> platform was added. Dynamic
-        architecture detection was enabled; OpenBLAS now selects
-        microarchitecture-optimized routines at runtime, so optimal
-        performance is achieved without the need to rebuild OpenBLAS
-        locally. OpenBLAS has replaced ATLAS in most packages which use
-        an optimized BLAS or LAPACK implementation.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The <literal>phpfpm</literal> is now using the default PHP
-        version (<literal>pkgs.php</literal>) instead of PHP 5.4
-        (<literal>pkgs.php54</literal>).
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The <literal>locate</literal> service no longer indexes the Nix
-        store by default, preventing packages with potentially numerous
-        versions from cluttering the output. Indexing the store can be
-        activated by setting
-        <literal>services.locate.includeStore = true</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The Nix expression search path (<literal>NIX_PATH</literal>) no
-        longer contains <literal>/etc/nixos/nixpkgs</literal> by
-        default. You can override <literal>NIX_PATH</literal> by setting
-        <literal>nix.nixPath</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Python 2.6 has been marked as broken (as it no longer receives
-        security updates from upstream).
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Any use of module arguments such as <literal>pkgs</literal> to
-        access library functions, or to define
-        <literal>imports</literal> attributes will now lead to an
-        infinite loop at the time of the evaluation.
-      </para>
-      <para>
-        In case of an infinite loop, use the
-        <literal>--show-trace</literal> command line argument and read
-        the line just above the error message.
-      </para>
-      <programlisting>
-$ nixos-rebuild build --show-trace
-…
-while evaluating the module argument `pkgs' in &quot;/etc/nixos/my-module.nix&quot;:
-infinite recursion encountered
-</programlisting>
-      <para>
-        Any use of <literal>pkgs.lib</literal>, should be replaced by
-        <literal>lib</literal>, after adding it as argument of the
-        module. The following module
-      </para>
-      <programlisting language="bash">
-{ config, pkgs, ... }:
-
-with pkgs.lib;
-
-{
-  options = {
-    foo = mkOption { … };
-  };
-  config = mkIf config.foo { … };
-}
-</programlisting>
-      <para>
-        should be modified to look like:
-      </para>
-      <programlisting language="bash">
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-{
-  options = {
-    foo = mkOption { option declaration };
-  };
-  config = mkIf config.foo { option definition };
-}
-</programlisting>
-      <para>
-        When <literal>pkgs</literal> is used to download other projects
-        to import their modules, and only in such cases, it should be
-        replaced by <literal>(import &lt;nixpkgs&gt; {})</literal>. The
-        following module
-      </para>
-      <programlisting language="bash">
-{ config, pkgs, ... }:
-
-let
-  myProject = pkgs.fetchurl {
-    src = url;
-    sha256 = hash;
-  };
-in
-
-{
-  imports = [ &quot;${myProject}/module.nix&quot; ];
-}
-</programlisting>
-      <para>
-        should be modified to look like:
-      </para>
-      <programlisting language="bash">
-{ config, pkgs, ... }:
-
-let
-  myProject = (import &lt;nixpkgs&gt; {}).fetchurl {
-    src = url;
-    sha256 = hash;
-  };
-in
-
-{
-  imports = [ &quot;${myProject}/module.nix&quot; ];
-}
-</programlisting>
-    </listitem>
-  </itemizedlist>
-  <para>
-    Other notable improvements:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        The nixos and nixpkgs channels were unified, so one
-        <emphasis>can</emphasis> use
-        <literal>nix-env -iA nixos.bash</literal> instead of
-        <literal>nix-env -iA nixos.pkgs.bash</literal>. See
-        <link xlink:href="https://github.com/NixOS/nixpkgs/commit/2cd7c1f198">the
-        commit</link> for details.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Users running an SSH server who worry about the quality of their
-        <literal>/etc/ssh/moduli</literal> file with respect to the
-        <link xlink:href="https://stribika.github.io/2015/01/04/secure-secure-shell.html">vulnerabilities
-        discovered in the Diffie-Hellman key exchange</link> can now
-        replace OpenSSH's default version with one they generated
-        themselves using the new
-        <literal>services.openssh.moduliFile</literal> option.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        A newly packaged TeX Live 2015 is provided in
-        <literal>pkgs.texlive</literal>, split into 6500 nix packages.
-        For basic user documentation see
-        <link xlink:href="https://github.com/NixOS/nixpkgs/blob/release-15.09/pkgs/tools/typesetting/tex/texlive/default.nix#L1">the
-        source</link>. Beware of
-        <link xlink:href="https://github.com/NixOS/nixpkgs/issues/9757">an
-        issue</link> when installing a too large package set. The plan
-        is to deprecate and maybe delete the original TeX packages until
-        the next release.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>buildEnv.env</literal> on all Python interpreters is
-        now available for nix-shell interoperability.
-      </para>
-    </listitem>
-  </itemizedlist>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1603.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1603.section.xml
deleted file mode 100644
index afbd2fd2c79..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1603.section.xml
+++ /dev/null
@@ -1,695 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-16.03">
-  <title>Release 16.03 (<quote>Emu</quote>, 2016/03/31)</title>
-  <para>
-    In addition to numerous new and upgraded packages, this release has
-    the following highlights:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        Systemd 229, bringing
-        <link xlink:href="https://github.com/systemd/systemd/blob/v229/NEWS">numerous
-        improvements</link> over 217.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Linux 4.4 (was 3.18).
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        GCC 5.3 (was 4.9). Note that GCC 5
-        <link xlink:href="https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_dual_abi.html">changes
-        the C++ ABI in an incompatible way</link>; this may cause
-        problems if you try to link objects compiled with different
-        versions of GCC.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Glibc 2.23 (was 2.21).
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Binutils 2.26 (was 2.23.1). See #909
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Improved support for ensuring
-        <link xlink:href="https://reproducible-builds.org/">bitwise
-        reproducible builds</link>. For example,
-        <literal>stdenv</literal> now sets the environment variable
-        <literal>SOURCE_DATE_EPOCH</literal> to a deterministic value,
-        and Nix has
-        <link xlink:href="https://nixos.org/nix/manual/#ssec-relnotes-1.11">gained
-        an option</link> to repeat a build a number of times to test
-        determinism. An ongoing project, the goal of exact
-        reproducibility is to allow binaries to be verified
-        independently (e.g., a user might only trust binaries that
-        appear in three independent binary caches).
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Perl 5.22.
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    The following new services were added since the last release:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        <literal>services/monitoring/longview.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>hardware/video/webcam/facetimehd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>i18n/input-method/default.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>i18n/input-method/fcitx.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>i18n/input-method/ibus.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>i18n/input-method/nabi.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>i18n/input-method/uim.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>programs/fish.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>security/acme.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>security/audit.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>security/oath.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/hardware/irqbalance.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/mail/dspam.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/mail/opendkim.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/mail/postsrsd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/mail/rspamd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/mail/rmilter.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/autofs.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/bepasty.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/calibre-server.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/cfdyndns.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/gammu-smsd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/mathics.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/matrix-synapse.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/misc/octoprint.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/monitoring/hdaps.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/monitoring/heapster.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/monitoring/longview.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/network-filesystems/netatalk.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/network-filesystems/xtreemfs.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/autossh.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/dnschain.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/gale.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/miniupnpd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/namecoind.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/ostinato.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/pdnsd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/shairport-sync.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/networking/supplicant.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/search/kibana.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/security/haka.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/security/physlock.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/web-apps/pump.io.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/x11/hardware/libinput.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services/x11/window-managers/windowlab.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/initrd-network.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/initrd-ssh.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/loader/loader.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/networkd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>system/boot/resolved.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>virtualisation/lxd.nix</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>virtualisation/rkt.nix</literal>
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    When upgrading from a previous release, please be aware of the
-    following incompatible changes:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        We no longer produce graphical ISO images and VirtualBox images
-        for <literal>i686-linux</literal>. A minimal ISO image is still
-        provided.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Firefox and similar browsers are now <emphasis>wrapped by
-        default</emphasis>. The package and attribute names are plain
-        <literal>firefox</literal> or <literal>midori</literal>, etc.
-        Backward-compatibility attributes were set up, but note that
-        <literal>nix-env -u</literal> will <emphasis>not</emphasis>
-        update your current <literal>firefox-with-plugins</literal>; you
-        have to uninstall it and install <literal>firefox</literal>
-        instead.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>wmiiSnap</literal> has been replaced with
-        <literal>wmii_hg</literal>, but
-        <literal>services.xserver.windowManager.wmii.enable</literal>
-        has been updated respectively so this only affects you if you
-        have explicitly installed <literal>wmiiSnap</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>jobs</literal> NixOS option has been removed. It served
-        as compatibility layer between Upstart jobs and SystemD
-        services. All services have been rewritten to use
-        <literal>systemd.services</literal>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>wmiimenu</literal> is removed, as it has been removed
-        by the developers upstream. Use <literal>wimenu</literal> from
-        the <literal>wmii-hg</literal> package.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Gitit is no longer automatically added to the module list in
-        NixOS and as such there will not be any manual entries for it.
-        You will need to add an import statement to your NixOS
-        configuration in order to use it, e.g.
-      </para>
-      <programlisting language="bash">
-{
-  imports = [ &lt;nixpkgs/nixos/modules/services/misc/gitit.nix&gt; ];
-}
-</programlisting>
-      <para>
-        will include the Gitit service configuration options.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>nginx</literal> does not accept flags for enabling and
-        disabling modules anymore. Instead it accepts
-        <literal>modules</literal> argument, which is a list of modules
-        to be built in. All modules now reside in
-        <literal>nginxModules</literal> set. Example configuration:
-      </para>
-      <programlisting language="bash">
-nginx.override {
-  modules = [ nginxModules.rtmp nginxModules.dav nginxModules.moreheaders ];
-}
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>s3sync</literal> is removed, as it hasn't been
-        developed by upstream for 4 years and only runs with ruby 1.8.
-        For an actively-developer alternative look at
-        <literal>tarsnap</literal> and others.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>ruby_1_8</literal> has been removed as it's not
-        supported from upstream anymore and probably contains security
-        issues.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>tidy-html5</literal> package is removed. Upstream only
-        provided <literal>(lib)tidy5</literal> during development, and
-        now they went back to <literal>(lib)tidy</literal> to work as a
-        drop-in replacement of the original package that has been
-        unmaintained for years. You can (still) use the
-        <literal>html-tidy</literal> package, which got updated to a
-        stable release from this new upstream.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>extraDeviceOptions</literal> argument is removed from
-        <literal>bumblebee</literal> package. Instead there are now two
-        separate arguments: <literal>extraNvidiaDeviceOptions</literal>
-        and <literal>extraNouveauDeviceOptions</literal> for setting
-        extra X11 options for nvidia and nouveau drivers, respectively.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The <literal>Ctrl+Alt+Backspace</literal> key combination no
-        longer kills the X server by default. There's a new option
-        <literal>services.xserver.enableCtrlAltBackspace</literal>
-        allowing to enable the combination again.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>emacsPackagesNg</literal> now contains all packages
-        from the ELPA, MELPA, and MELPA Stable repositories.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Data directory for Postfix MTA server is moved from
-        <literal>/var/postfix</literal> to
-        <literal>/var/lib/postfix</literal>. Old configurations are
-        migrated automatically. <literal>service.postfix</literal>
-        module has also received many improvements, such as correct
-        directories' access rights, new <literal>aliasFiles</literal>
-        and <literal>mapFiles</literal> options and more.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Filesystem options should now be configured as a list of
-        strings, not a comma-separated string. The old style will
-        continue to work, but print a warning, until the 16.09 release.
-        An example of the new style:
-      </para>
-      <programlisting language="bash">
-{
-  fileSystems.&quot;/example&quot; = {
-    device = &quot;/dev/sdc&quot;;
-    fsType = &quot;btrfs&quot;;
-    options = [ &quot;noatime&quot; &quot;compress=lzo&quot; &quot;space_cache&quot; &quot;autodefrag&quot; ];
-  };
-}
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        CUPS, installed by <literal>services.printing</literal> module,
-        now has its data directory in <literal>/var/lib/cups</literal>.
-        Old configurations from <literal>/etc/cups</literal> are moved
-        there automatically, but there might be problems. Also
-        configuration options
-        <literal>services.printing.cupsdConf</literal> and
-        <literal>services.printing.cupsdFilesConf</literal> were removed
-        because they had been allowing one to override configuration
-        variables required for CUPS to work at all on NixOS. For most
-        use cases, <literal>services.printing.extraConf</literal> and
-        new option <literal>services.printing.extraFilesConf</literal>
-        should be enough; if you encounter a situation when they are
-        not, please file a bug.
-      </para>
-      <para>
-        There are also Gutenprint improvements; in particular, a new
-        option <literal>services.printing.gutenprint</literal> is added
-        to enable automatic updating of Gutenprint PPMs; it's greatly
-        recommended to enable it instead of adding
-        <literal>gutenprint</literal> to the <literal>drivers</literal>
-        list.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services.xserver.vaapiDrivers</literal> has been
-        removed. Use
-        <literal>hardware.opengl.extraPackages{,32}</literal> instead.
-        You can also specify VDPAU drivers there.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>programs.ibus</literal> moved to
-        <literal>i18n.inputMethod.ibus</literal>. The option
-        <literal>programs.ibus.plugins</literal> changed to
-        <literal>i18n.inputMethod.ibus.engines</literal> and the option
-        to enable ibus changed from
-        <literal>programs.ibus.enable</literal> to
-        <literal>i18n.inputMethod.enabled</literal>.
-        <literal>i18n.inputMethod.enabled</literal> should be set to the
-        used input method name, <literal>&quot;ibus&quot;</literal> for
-        ibus. An example of the new style:
-      </para>
-      <programlisting language="bash">
-{
-  i18n.inputMethod.enabled = &quot;ibus&quot;;
-  i18n.inputMethod.ibus.engines = with pkgs.ibus-engines; [ anthy mozc ];
-}
-</programlisting>
-      <para>
-        That is equivalent to the old version:
-      </para>
-      <programlisting language="bash">
-{
-  programs.ibus.enable = true;
-  programs.ibus.plugins = with pkgs; [ ibus-anthy mozc ];
-}
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services.udev.extraRules</literal> option now writes
-        rules to <literal>99-local.rules</literal> instead of
-        <literal>10-local.rules</literal>. This makes all the user rules
-        apply after others, so their results wouldn't be overridden by
-        anything else.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Large parts of the <literal>services.gitlab</literal> module has
-        been been rewritten. There are new configuration options
-        available. The <literal>stateDir</literal> option was renamned
-        to <literal>statePath</literal> and the
-        <literal>satellitesDir</literal> option was removed. Please
-        review the currently available options.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The option
-        <literal>services.nsd.zones.&lt;name&gt;.data</literal> no
-        longer interpret the dollar sign ($) as a shell variable, as
-        such it should not be escaped anymore. Thus the following zone
-        data:
-      </para>
-      <programlisting>
-$ORIGIN example.com.
-$TTL 1800
-@       IN      SOA     ns1.vpn.nbp.name.      admin.example.com. (
-</programlisting>
-      <para>
-        Should modified to look like the actual file expected by nsd:
-      </para>
-      <programlisting>
-$ORIGIN example.com.
-$TTL 1800
-@       IN      SOA     ns1.vpn.nbp.name.      admin.example.com. (
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>service.syncthing.dataDir</literal> options now has to
-        point to exact folder where syncthing is writing to. Example
-        configuration should look something like:
-      </para>
-      <programlisting language="bash">
-{
-  services.syncthing = {
-      enable = true;
-      dataDir = &quot;/home/somebody/.syncthing&quot;;
-      user = &quot;somebody&quot;;
-  };
-}
-</programlisting>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>networking.firewall.allowPing</literal> is now enabled
-        by default. Users are encouraged to configure an appropriate
-        rate limit for their machines using the Kernel interface at
-        <literal>/proc/sys/net/ipv4/icmp_ratelimit</literal> and
-        <literal>/proc/sys/net/ipv6/icmp/ratelimit</literal> or using
-        the firewall itself, i.e. by setting the NixOS option
-        <literal>networking.firewall.pingLimit</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Systems with some broadcom cards used to result into a generated
-        config that is no longer accepted. If you get errors like
-      </para>
-      <programlisting>
-error: path ‘/nix/store/*-broadcom-sta-*’ does not exist and cannot be created
-</programlisting>
-      <para>
-        you should either re-run
-        <literal>nixos-generate-config</literal> or manually replace
-        <literal>&quot;${config.boot.kernelPackages.broadcom_sta}&quot;</literal>
-        by <literal>config.boot.kernelPackages.broadcom_sta</literal> in
-        your <literal>/etc/nixos/hardware-configuration.nix</literal>.
-        More discussion is on
-        <link xlink:href="https://github.com/NixOS/nixpkgs/pull/12595">
-        the github issue</link>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The <literal>services.xserver.startGnuPGAgent</literal> option
-        has been removed. GnuPG 2.1.x changed the way the gpg-agent
-        works, and that new approach no longer requires (or even
-        supports) the &quot;start everything as a child of the
-        agent&quot; scheme we've implemented in NixOS for older
-        versions. To configure the gpg-agent for your X session, add the
-        following code to <literal>~/.bashrc</literal> or some file
-        that’s sourced when your shell is started:
-      </para>
-      <programlisting>
-GPG_TTY=$(tty)
-export GPG_TTY
-</programlisting>
-      <para>
-        If you want to use gpg-agent for SSH, too, add the following to
-        your session initialization (e.g.
-        <literal>displayManager.sessionCommands</literal>)
-      </para>
-      <programlisting>
-    gpg-connect-agent /bye
-    unset SSH_AGENT_PID
-    export SSH_AUTH_SOCK=&quot;''${HOME}/.gnupg/S.gpg-agent.ssh&quot;
-</programlisting>
-      <para>
-        and make sure that
-      </para>
-      <programlisting>
-    enable-ssh-support
-</programlisting>
-      <para>
-        is included in your <literal>~/.gnupg/gpg-agent.conf</literal>.
-        You will need to use <literal>ssh-add</literal> to re-add your
-        ssh keys. If gpg’s automatic transformation of the private keys
-        to the new format fails, you will need to re-import your private
-        keyring as well:
-      </para>
-      <programlisting>
-    gpg --import ~/.gnupg/secring.gpg
-</programlisting>
-      <para>
-        The <literal>gpg-agent(1)</literal> man page has more details
-        about this subject, i.e. in the &quot;EXAMPLES&quot; section.
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    Other notable improvements:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        <literal>ejabberd</literal> module is brought back and now works
-        on NixOS.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Input method support was improved. New NixOS modules (fcitx,
-        nabi and uim), fcitx engines (chewing, hangul, m17n, mozc and
-        table-other) and ibus engines (hangul and m17n) have been added.
-      </para>
-    </listitem>
-  </itemizedlist>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1609.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1609.section.xml
deleted file mode 100644
index 0fba40a0e78..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1609.section.xml
+++ /dev/null
@@ -1,273 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-16.09">
-  <title>Release 16.09 (<quote>Flounder</quote>, 2016/09/30)</title>
-  <para>
-    In addition to numerous new and upgraded packages, this release has
-    the following highlights:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        Many NixOS configurations and Nix packages now use significantly
-        less disk space, thanks to the
-        <link xlink:href="https://github.com/NixOS/nixpkgs/issues/7117">extensive
-        work on closure size reduction</link>. For example, the closure
-        size of a minimal NixOS container went down from ~424 MiB in
-        16.03 to ~212 MiB in 16.09, while the closure size of Firefox
-        went from ~651 MiB to ~259 MiB.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        To improve security, packages are now
-        <link xlink:href="https://github.com/NixOS/nixpkgs/pull/12895">built
-        using various hardening features</link>. See the Nixpkgs manual
-        for more information.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Support for PXE netboot. See
-        <xref linkend="sec-booting-from-pxe" /> for documentation.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        X.org server 1.18. If you use the <literal>ati_unfree</literal>
-        driver, 1.17 is still used due to an ABI incompatibility.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        This release is based on Glibc 2.24, GCC 5.4.0 and systemd 231.
-        The default Linux kernel remains 4.4.
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    The following new services were added since the last release:
-  </para>
-  <itemizedlist spacing="compact">
-    <listitem>
-      <para>
-        <literal>(this will get automatically generated at release time)</literal>
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    When upgrading from a previous release, please be aware of the
-    following incompatible changes:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        A large number of packages have been converted to use the
-        multiple outputs feature of Nix to greatly reduce the amount of
-        required disk space, as mentioned above. This may require
-        changes to any custom packages to make them build again; see the
-        relevant chapter in the Nixpkgs manual for more information.
-        (Additional caveat to packagers: some packaging conventions
-        related to multiple-output packages
-        <link xlink:href="https://github.com/NixOS/nixpkgs/pull/14766">were
-        changed</link> late (August 2016) in the release cycle and
-        differ from the initial introduction of multiple outputs.)
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Previous versions of Nixpkgs had support for all versions of the
-        LTS Haskell package set. That support has been dropped. The
-        previously provided <literal>haskell.packages.lts-x_y</literal>
-        package sets still exist in name to aviod breaking user code,
-        but these package sets don't actually contain the versions
-        mandated by the corresponding LTS release. Instead, our package
-        set it loosely based on the latest available LTS release, i.e.
-        LTS 7.x at the time of this writing. New releases of NixOS and
-        Nixpkgs will drop those old names entirely.
-        <link xlink:href="https://nixos.org/nix-dev/2016-June/020585.html">The
-        motivation for this change</link> has been discussed at length
-        on the <literal>nix-dev</literal> mailing list and in
-        <link xlink:href="https://github.com/NixOS/nixpkgs/issues/14897">Github
-        issue #14897</link>. Development strategies for Haskell hackers
-        who want to rely on Nix and NixOS have been described in
-        <link xlink:href="https://nixos.org/nix-dev/2016-June/020642.html">another
-        nix-dev article</link>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Shell aliases for systemd sub-commands
-        <link xlink:href="https://github.com/NixOS/nixpkgs/pull/15598">were
-        dropped</link>: <literal>start</literal>,
-        <literal>stop</literal>, <literal>restart</literal>,
-        <literal>status</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Redis now binds to 127.0.0.1 only instead of listening to all
-        network interfaces. This is the default behavior of Redis 3.2
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>/var/empty</literal> is now immutable. Activation
-        script runs <literal>chattr +i</literal> to forbid any
-        modifications inside the folder. See
-        <link xlink:href="https://github.com/NixOS/nixpkgs/pull/18365">
-        the pull request</link> for what bugs this caused.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Gitlab's maintainance script <literal>gitlab-runner</literal>
-        was removed and split up into the more clearer
-        <literal>gitlab-run</literal> and <literal>gitlab-rake</literal>
-        scripts, because <literal>gitlab-runner</literal> is a component
-        of Gitlab CI.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services.xserver.libinput.accelProfile</literal>
-        default changed from <literal>flat</literal> to
-        <literal>adaptive</literal>, as per
-        <link xlink:href="https://wayland.freedesktop.org/libinput/doc/latest/group__config.html#gad63796972347f318b180e322e35cee79">
-        official documentation</link>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>fonts.fontconfig.ultimate.rendering</literal> was
-        removed because our presets were obsolete for some time. New
-        presets are hardcoded into FreeType; you can select a preset via
-        <literal>fonts.fontconfig.ultimate.preset</literal>. You can
-        customize those presets via ordinary environment variables,
-        using <literal>environment.variables</literal>.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The <literal>audit</literal> service is no longer enabled by
-        default. Use <literal>security.audit.enable = true</literal> to
-        explicitly enable it.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>pkgs.linuxPackages.virtualbox</literal> now contains
-        only the kernel modules instead of the VirtualBox user space
-        binaries. If you want to reference the user space binaries, you
-        have to use the new <literal>pkgs.virtualbox</literal> instead.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>goPackages</literal> was replaced with separated Go
-        applications in appropriate <literal>nixpkgs</literal>
-        categories. Each Go package uses its own dependency set. There's
-        also a new <literal>go2nix</literal> tool introduced to generate
-        a Go package definition from its Go source automatically.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services.mongodb.extraConfig</literal> configuration
-        format was changed to YAML.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        PHP has been upgraded to 7.0
-      </para>
-    </listitem>
-  </itemizedlist>
-  <para>
-    Other notable improvements:
-  </para>
-  <itemizedlist>
-    <listitem>
-      <para>
-        Revamped grsecurity/PaX support. There is now only a single
-        general-purpose distribution kernel and the configuration
-        interface has been streamlined. Desktop users should be able to
-        simply set
-      </para>
-      <programlisting language="bash">
-{
-  security.grsecurity.enable = true;
-}
-</programlisting>
-      <para>
-        to get a reasonably secure system without having to sacrifice
-        too much functionality.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Special filesystems, like <literal>/proc</literal>,
-        <literal>/run</literal> and others, now have the same mount
-        options as recommended by systemd and are unified across
-        different places in NixOS. Mount options are updated during
-        <literal>nixos-rebuild switch</literal> if possible. One benefit
-        from this is improved security — most such filesystems are now
-        mounted with <literal>noexec</literal>, <literal>nodev</literal>
-        and/or <literal>nosuid</literal> options.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The reverse path filter was interfering with DHCPv4 server
-        operation in the past. An exception for DHCPv4 and a new option
-        to log packets that were dropped due to the reverse path filter
-        was added
-        (<literal>networking.firewall.logReversePathDrops</literal>) for
-        easier debugging.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Containers configuration within
-        <literal>containers.&lt;name&gt;.config</literal> is
-        <link xlink:href="https://github.com/NixOS/nixpkgs/pull/17365">now
-        properly typed and checked</link>. In particular, partial
-        configurations are merged correctly.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        The directory container setuid wrapper programs,
-        <literal>/var/setuid-wrappers</literal>,
-        <link xlink:href="https://github.com/NixOS/nixpkgs/pull/18124">is
-        now updated atomically to prevent failures if the switch to a
-        new configuration is interrupted.</link>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <literal>services.xserver.startGnuPGAgent</literal> has been
-        removed due to GnuPG 2.1.x bump. See
-        <link xlink:href="https://github.com/NixOS/nixpkgs/commit/5391882ebd781149e213e8817fba6ac3c503740c">
-        how to achieve similar behavior</link>. You might need to
-        <literal>pkill gpg-agent</literal> after the upgrade to prevent
-        a stale agent being in the way.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        <link xlink:href="https://github.com/NixOS/nixpkgs/commit/e561edc322d275c3687fec431935095cfc717147">
-        Declarative users could share the uid due to the bug in the
-        script handling conflict resolution. </link>
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Gummi boot has been replaced using systemd-boot.
-      </para>
-    </listitem>
-    <listitem>
-      <para>
-        Hydra package and NixOS module were added for convenience.
-      </para>
-    </listitem>
-  </itemizedlist>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1703.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1703.section.xml
deleted file mode 100644
index 1119ec53dfc..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1703.section.xml
+++ /dev/null
@@ -1,818 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-17.03">
-  <title>Release 17.03 (<quote>Gorilla</quote>, 2017/03/31)</title>
-  <section xml:id="sec-release-17.03-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      has the following highlights:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Nixpkgs is now extensible through overlays. See the
-          <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-overlays-install">Nixpkgs
-          manual</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          This release is based on Glibc 2.25, GCC 5.4.0 and systemd
-          232. The default Linux kernel is 4.9 and Nix is at 1.11.8.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default desktop environment now is KDE's Plasma 5. KDE 4
-          has been removed
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The setuid wrapper functionality now supports setting
-          capabilities.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          X.org server uses branch 1.19. Due to ABI incompatibilities,
-          <literal>ati_unfree</literal> keeps forcing 1.17 and
-          <literal>amdgpu-pro</literal> starts forcing 1.18.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Cross compilation has been rewritten. See the nixpkgs manual
-          for details. The most obvious breaking change is that in
-          derivations there is no <literal>.nativeDrv</literal> nor
-          <literal>.crossDrv</literal> are now cross by default, not
-          native.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>overridePackages</literal> function has been
-          rewritten to be replaced by
-          <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-overlays-install">
-          overlays</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Packages in nixpkgs can be marked as insecure through listed
-          vulnerabilities. See the
-          <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-allow-insecure">Nixpkgs
-          manual</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PHP now defaults to PHP 7.1
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-17.03-new-services">
-    <title>New Services</title>
-    <para>
-      The following new services were added since the last release:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>hardware/ckb.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>hardware/mcelog.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>hardware/usb-wwan.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>hardware/video/capture/mwprocapture.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/adb.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/chromium.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/gphoto2.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/java.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/mtr.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/oblogout.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/vim.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/wireshark.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>security/dhparams.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/audio/ympd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/computing/boinc/client.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/continuous-integration/buildbot/master.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/continuous-integration/buildbot/worker.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/continuous-integration/gitlab-runner.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/databases/riak-cs.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/databases/stanchion.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/desktops/gnome3/gnome-terminal-server.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/editors/infinoted.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/hardware/illum.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/hardware/trezord.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/logging/journalbeat.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/mail/offlineimap.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/mail/postgrey.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/couchpotato.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/docker-registry.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/errbot.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/geoip-updater.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/gogs.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/leaps.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/nix-optimise.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/ssm-agent.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/sssd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/arbtt.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/netdata.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/default.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/alertmanager.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/blackbox-exporter.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/json-exporter.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/nginx-exporter.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/node-exporter.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/snmp-exporter.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/unifi-exporter.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/varnish-exporter.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/sysstat.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/telegraf.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/vnstat.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/network-filesystems/cachefilesd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/network-filesystems/glusterfs.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/network-filesystems/ipfs.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/dante.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/dnscrypt-wrapper.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/fakeroute.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/flannel.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/htpdate.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/miredo.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/nftables.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/powerdns.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/pdns-recursor.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/quagga.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/redsocks.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/wireguard.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/system/cgmanager.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/torrent/opentracker.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-apps/atlassian/confluence.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-apps/atlassian/crowd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-apps/atlassian/jira.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-apps/frab.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-apps/nixbot.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-apps/selfoss.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-apps/quassel-webserver.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/x11/unclutter-xfixes.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/x11/urxvtd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>system/boot/systemd-nspawn.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>virtualisation/ecs-agent.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>virtualisation/lxcfs.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>virtualisation/openstack/keystone.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>virtualisation/openstack/glance.nix</literal>
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-17.03-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <para>
-      When upgrading from a previous release, please be aware of the
-      following incompatible changes:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Derivations have no <literal>.nativeDrv</literal> nor
-          <literal>.crossDrv</literal> and are now cross by default, not
-          native.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>stdenv.overrides</literal> is now expected to take
-          <literal>self</literal> and <literal>super</literal>
-          arguments. See <literal>lib.trivial.extends</literal> for what
-          those parameters represent.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>ansible</literal> now defaults to ansible version 2
-          as version 1 has been removed due to a serious
-          <link xlink:href="https://www.computest.nl/advisories/CT-2017-0109_Ansible.txt">
-          vulnerability</link> unpatched by upstream.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>gnome</literal> alias has been removed along with
-          <literal>gtk</literal>, <literal>gtkmm</literal> and several
-          others. Now you need to use versioned attributes, like
-          <literal>gnome3</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The attribute name of the Radicale daemon has been changed
-          from <literal>pythonPackages.radicale</literal> to
-          <literal>radicale</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>stripHash</literal> bash function in
-          <literal>stdenv</literal> changed according to its
-          documentation; it now outputs the stripped name to
-          <literal>stdout</literal> instead of putting it in the
-          variable <literal>strippedName</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PHP now scans for extra configuration .ini files in /etc/php.d
-          instead of /etc. This prevents accidentally loading non-PHP
-          .ini files that may be in /etc.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Two lone top-level dict dbs moved into
-          <literal>dictdDBs</literal>. This affects:
-          <literal>dictdWordnet</literal> which is now at
-          <literal>dictdDBs.wordnet</literal> and
-          <literal>dictdWiktionary</literal> which is now at
-          <literal>dictdDBs.wiktionary</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Parsoid service now uses YAML configuration format.
-          <literal>service.parsoid.interwikis</literal> is now called
-          <literal>service.parsoid.wikis</literal> and is a list of
-          either API URLs or attribute sets as specified in parsoid's
-          documentation.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>Ntpd</literal> was replaced by
-          <literal>systemd-timesyncd</literal> as the default service to
-          synchronize system time with a remote NTP server. The old
-          behavior can be restored by setting
-          <literal>services.ntp.enable</literal> to
-          <literal>true</literal>. Upstream time servers for all NTP
-          implementations are now configured using
-          <literal>networking.timeServers</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>service.nylon</literal> is now declared using named
-          instances. As an example:
-        </para>
-        <programlisting language="bash">
-{
-  services.nylon = {
-    enable = true;
-    acceptInterface = &quot;br0&quot;;
-    bindInterface = &quot;tun1&quot;;
-    port = 5912;
-  };
-}
-</programlisting>
-        <para>
-          should be replaced with:
-        </para>
-        <programlisting language="bash">
-{
-  services.nylon.myvpn = {
-    enable = true;
-    acceptInterface = &quot;br0&quot;;
-    bindInterface = &quot;tun1&quot;;
-    port = 5912;
-  };
-}
-</programlisting>
-        <para>
-          this enables you to declare a SOCKS proxy for each uplink.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>overridePackages</literal> function no longer exists.
-          It is replaced by
-          <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-overlays-install">
-          overlays</link>. For example, the following code:
-        </para>
-        <programlisting language="bash">
-let
-  pkgs = import &lt;nixpkgs&gt; {};
-in
-  pkgs.overridePackages (self: super: ...)
-</programlisting>
-        <para>
-          should be replaced by:
-        </para>
-        <programlisting language="bash">
-let
-  pkgs = import &lt;nixpkgs&gt; {};
-in
-  import pkgs.path { overlays = [(self: super: ...)]; }
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          Autoloading connection tracking helpers is now disabled by
-          default. This default was also changed in the Linux kernel and
-          is considered insecure if not configured properly in your
-          firewall. If you need connection tracking helpers (i.e. for
-          active FTP) please enable
-          <literal>networking.firewall.autoLoadConntrackHelpers</literal>
-          and tune
-          <literal>networking.firewall.connectionTrackingModules</literal>
-          to suit your needs.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>local_recipient_maps</literal> is not set to empty
-          value by Postfix service. It's an insecure default as stated
-          by Postfix documentation. Those who want to retain this
-          setting need to set it via
-          <literal>services.postfix.extraConfig</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Iputils no longer provide ping6 and traceroute6. The
-          functionality of these tools has been integrated into ping and
-          traceroute respectively. To enforce an address family the new
-          flags <literal>-4</literal> and <literal>-6</literal> have
-          been added. One notable incompatibility is that specifying an
-          interface (for link-local IPv6 for instance) is no longer done
-          with the <literal>-I</literal> flag, but by encoding the
-          interface into the address
-          (<literal>ping fe80::1%eth0</literal>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The socket handling of the <literal>services.rmilter</literal>
-          module has been fixed and refactored. As rmilter doesn't
-          support binding to more than one socket, the options
-          <literal>bindUnixSockets</literal> and
-          <literal>bindInetSockets</literal> have been replaced by
-          <literal>services.rmilter.bindSocket.*</literal>. The default
-          is still a unix socket in
-          <literal>/run/rmilter/rmilter.sock</literal>. Refer to the
-          options documentation for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>fetch*</literal> functions no longer support md5,
-          please use sha256 instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The dnscrypt-proxy module interface has been streamlined
-          around the <literal>extraArgs</literal> option. Where
-          possible, legacy option declarations are mapped to
-          <literal>extraArgs</literal> but will emit warnings. The
-          <literal>resolverList</literal> has been outright removed: to
-          use an unlisted resolver, use the
-          <literal>customResolver</literal> option.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          torbrowser now stores local state under
-          <literal>~/.local/share/tor-browser</literal> by default. Any
-          browser profile data from the old location,
-          <literal>~/.torbrowser4</literal>, must be migrated manually.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The ihaskell, monetdb, offlineimap and sitecopy services have
-          been removed.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-17.03-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Module type system have a new extensible option types feature
-          that allow to extend certain types, such as enum, through
-          multiple option declarations of the same option across
-          multiple modules.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>jre</literal> now defaults to GTK UI by default. This
-          improves visual consistency and makes Java follow system font
-          style, improving the situation on HighDPI displays. This has a
-          cost of increased closure size; for server and other headless
-          workloads it's recommended to use
-          <literal>jre_headless</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Python 2.6 interpreter and package set have been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Python 2.7 interpreter does not use modules anymore.
-          Instead, all CPython interpreters now include the whole
-          standard library except for `tkinter`, which is available in
-          the Python package set.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Python 2.7, 3.5 and 3.6 are now built deterministically and
-          3.4 mostly. Minor modifications had to be made to the
-          interpreters in order to generate deterministic bytecode. This
-          has security implications and is relevant for those using
-          Python in a <literal>nix-shell</literal>. See the Nixpkgs
-          manual for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Python package sets now use a fixed-point combinator and
-          the sets are available as attributes of the interpreters.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Python function <literal>buildPythonPackage</literal> has
-          been improved and can be used to build from Setuptools source,
-          Flit source, and precompiled Wheels.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          When adding new or updating current Python libraries, the
-          expressions should be put in separate files in
-          <literal>pkgs/development/python-modules</literal> and called
-          from <literal>python-packages.nix</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The dnscrypt-proxy service supports synchronizing the list of
-          public resolvers without working DNS resolution. This fixes
-          issues caused by the resolver list becoming outdated. It also
-          improves the viability of DNSCrypt only configurations.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Containers using bridged networking no longer lose their
-          connection after changes to the host networking.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          ZFS supports pool auto scrubbing.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The bind DNS utilities (e.g. dig) have been split into their
-          own output and are now also available in
-          <literal>pkgs.dnsutils</literal> and it is no longer necessary
-          to pull in all of <literal>bind</literal> to use them.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Per-user configuration was moved from
-          <literal>~/.nixpkgs</literal> to
-          <literal>~/.config/nixpkgs</literal>. The former is still
-          valid for <literal>config.nix</literal> for backwards
-          compatibility.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1709.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1709.section.xml
deleted file mode 100644
index fc5d11f07c8..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1709.section.xml
+++ /dev/null
@@ -1,922 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-17.09">
-  <title>Release 17.09 (<quote>Hummingbird</quote>, 2017/09/??)</title>
-  <section xml:id="sec-release-17.09-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      has the following highlights:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The GNOME version is now 3.24. KDE Plasma was upgraded to
-          5.10, KDE Applications to 17.08.1 and KDE Frameworks to 5.37.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The user handling now keeps track of deallocated UIDs/GIDs.
-          When a user or group is revived, this allows it to be
-          allocated the UID/GID it had before. A consequence is that
-          UIDs and GIDs are no longer reused.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The module option
-          <literal>services.xserver.xrandrHeads</literal> now causes the
-          first head specified in this list to be set as the primary
-          head. Apart from that, it's now possible to also set
-          additional options by using an attribute set, for example:
-        </para>
-        <programlisting language="bash">
-{ services.xserver.xrandrHeads = [
-    &quot;HDMI-0&quot;
-    {
-      output = &quot;DVI-0&quot;;
-      primary = true;
-      monitorConfig = ''
-        Option &quot;Rotate&quot; &quot;right&quot;
-      '';
-    }
-  ];
-}
-</programlisting>
-        <para>
-          This will set the <literal>DVI-0</literal> output to be the
-          primary head, even though <literal>HDMI-0</literal> is the
-          first head in the list.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The handling of SSL in the <literal>services.nginx</literal>
-          module has been cleaned up, renaming the misnamed
-          <literal>enableSSL</literal> to <literal>onlySSL</literal>
-          which reflects its original intention. This is not to be used
-          with the already existing <literal>forceSSL</literal> which
-          creates a second non-SSL virtual host redirecting to the SSL
-          virtual host. This by chance had worked earlier due to
-          specific implementation details. In case you had specified
-          both please remove the <literal>enableSSL</literal> option to
-          keep the previous behaviour.
-        </para>
-        <para>
-          Another <literal>addSSL</literal> option has been introduced
-          to configure both a non-SSL virtual host and an SSL virtual
-          host with the same configuration.
-        </para>
-        <para>
-          Options to configure <literal>resolver</literal> options and
-          <literal>upstream</literal> blocks have been introduced. See
-          their information for further details.
-        </para>
-        <para>
-          The <literal>port</literal> option has been replaced by a more
-          generic <literal>listen</literal> option which makes it
-          possible to specify multiple addresses, ports and SSL configs
-          dependant on the new SSL handling mentioned above.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-17.09-new-services">
-    <title>New Services</title>
-    <para>
-      The following new services were added since the last release:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>config/fonts/fontconfig-penultimate.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>config/fonts/fontconfig-ultimate.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>config/terminfo.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>hardware/sensor/iio.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>hardware/nitrokey.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>hardware/raid/hpsa.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/browserpass.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/gnupg.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/qt5ct.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/slock.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs/thefuck.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>security/auditd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>security/lock-kernel-modules.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>service-managers/docker.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>service-managers/trivial.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/admin/salt/master.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/admin/salt/minion.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/audio/slimserver.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/cluster/kubernetes/default.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/cluster/kubernetes/dns.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/cluster/kubernetes/dashboard.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/continuous-integration/hail.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/databases/clickhouse.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/databases/postage.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/desktops/gnome3/gnome-disks.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/desktops/gnome3/gpaste.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/logging/SystemdJournal2Gelf.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/logging/heartbeat.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/logging/journalwatch.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/logging/syslogd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/mail/mailhog.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/mail/nullmailer.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/airsonic.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/autorandr.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/exhibitor.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/fstrim.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/gollum.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/irkerd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/jackett.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/radarr.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/misc/snapper.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/osquery.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/collectd-exporter.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/monitoring/prometheus/fritzbox-exporter.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/network-filesystems/kbfs.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/dnscache.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/fireqos.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/iwd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/keepalived/default.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/keybase.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/lldpd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/matterbridge.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/squid.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/tinydns.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/networking/xrdp.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/security/shibboleth-sp.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/security/sks.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/security/sshguard.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/security/torify.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/security/usbguard.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/security/vault.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/system/earlyoom.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/system/saslauthd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-apps/nexus.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-apps/pgpkeyserver-lite.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-apps/piwik.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-servers/lighttpd/collectd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/web-servers/minio.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/x11/display-managers/xpra.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services/x11/xautolock.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>tasks/filesystems/bcachefs.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>tasks/powertop.nix</literal>
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-17.09-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <para>
-      When upgrading from a previous release, please be aware of the
-      following incompatible changes:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <emphasis role="strong">In an Qemu-based virtualization
-          environment, the network interface names changed from i.e.
-          <literal>enp0s3</literal> to
-          <literal>ens3</literal>.</emphasis>
-        </para>
-        <para>
-          This is due to a kernel configuration change. The new naming
-          is consistent with those of other Linux distributions with
-          systemd. See
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/29197">#29197</link>
-          for more information.
-        </para>
-        <para>
-          A machine is affected if the <literal>virt-what</literal> tool
-          either returns <literal>qemu</literal> or
-          <literal>kvm</literal> <emphasis>and</emphasis> has interface
-          names used in any part of its NixOS configuration, in
-          particular if a static network configuration with
-          <literal>networking.interfaces</literal> is used.
-        </para>
-        <para>
-          Before rebooting affected machines, please ensure:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              Change the interface names in your NixOS configuration.
-              The first interface will be called
-              <literal>ens3</literal>, the second one
-              <literal>ens8</literal> and starting from there
-              incremented by 1.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              After changing the interface names, rebuild your system
-              with <literal>nixos-rebuild boot</literal> to activate the
-              new configuration after a reboot. If you switch to the new
-              configuration right away you might lose network
-              connectivity! If using <literal>nixops</literal>, deploy
-              with <literal>nixops deploy --force-reboot</literal>.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The following changes apply if the
-          <literal>stateVersion</literal> is changed to 17.09 or higher.
-          For <literal>stateVersion = &quot;17.03&quot;</literal> or
-          lower the old behavior is preserved.
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              The <literal>postgres</literal> default version was
-              changed from 9.5 to 9.6.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The <literal>postgres</literal> superuser name has changed
-              from <literal>root</literal> to
-              <literal>postgres</literal> to more closely follow what
-              other Linux distributions are doing.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The <literal>postgres</literal> default
-              <literal>dataDir</literal> has changed from
-              <literal>/var/db/postgres</literal> to
-              <literal>/var/lib/postgresql/$psqlSchema</literal> where
-              $psqlSchema is 9.6 for example.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The <literal>mysql</literal> default
-              <literal>dataDir</literal> has changed from
-              <literal>/var/mysql</literal> to
-              <literal>/var/lib/mysql</literal>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Radicale's default package has changed from 1.x to 2.x.
-              Instructions to migrate can be found
-              <link xlink:href="http://radicale.org/1to2/"> here
-              </link>. It is also possible to use the newer version by
-              setting the <literal>package</literal> to
-              <literal>radicale2</literal>, which is done automatically
-              when <literal>stateVersion</literal> is 17.09 or higher.
-              The <literal>extraArgs</literal> option has been added to
-              allow passing the data migration arguments specified in
-              the instructions; see the <literal>radicale.nix</literal>
-              NixOS test for an example migration.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>aiccu</literal> package was removed. This is due
-          to SixXS <link xlink:href="https://www.sixxs.net/main/">
-          sunsetting</link> its IPv6 tunnel.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>fanctl</literal> package and
-          <literal>fan</literal> module have been removed due to the
-          developers not upstreaming their iproute2 patches and lagging
-          with compatibility to recent iproute2 versions.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Top-level <literal>idea</literal> package collection was
-          renamed. All JetBrains IDEs are now at
-          <literal>jetbrains</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>flexget</literal>'s state database cannot be upgraded
-          to its new internal format, requiring removal of any existing
-          <literal>db-config.sqlite</literal> which will be
-          automatically recreated.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>ipfs</literal> service now doesn't ignore the
-          <literal>dataDir</literal> option anymore. If you've ever set
-          this option to anything other than the default you'll have to
-          either unset it (so the default gets used) or migrate the old
-          data manually with
-        </para>
-        <programlisting>
-dataDir=&lt;valueOfDataDir&gt;
-mv /var/lib/ipfs/.ipfs/* $dataDir
-rmdir /var/lib/ipfs/.ipfs
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>caddy</literal> service was previously using an
-          extra <literal>.caddy</literal> directory in the data
-          directory specified with the <literal>dataDir</literal>
-          option. The contents of the <literal>.caddy</literal>
-          directory are now expected to be in the
-          <literal>dataDir</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>ssh-agent</literal> user service is not started
-          by default anymore. Use
-          <literal>programs.ssh.startAgent</literal> to enable it if
-          needed. There is also a new
-          <literal>programs.gnupg.agent</literal> module that creates a
-          <literal>gpg-agent</literal> user service. It can also serve
-          as a SSH agent if <literal>enableSSHSupport</literal> is set.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <literal>services.tinc.networks.&lt;name&gt;.listenAddress</literal>
-          option had a misleading name that did not correspond to its
-          behavior. It now correctly defines the ip to listen for
-          incoming connections on. To keep the previous behaviour, use
-          <literal>services.tinc.networks.&lt;name&gt;.bindToAddress</literal>
-          instead. Refer to the description of the options for more
-          details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>tlsdate</literal> package and module were removed.
-          This is due to the project being dead and not building with
-          openssl 1.1.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>wvdial</literal> package and module were removed.
-          This is due to the project being dead and not building with
-          openssl 1.1.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>cc-wrapper</literal>'s setup-hook now exports a
-          number of environment variables corresponding to binutils
-          binaries, (e.g. <literal>LD</literal>,
-          <literal>STRIP</literal>, <literal>RANLIB</literal>, etc).
-          This is done to prevent packages' build systems guessing,
-          which is harder to predict, especially when cross-compiling.
-          However, some packages have broken due to this—their build
-          systems either not supporting, or claiming to support without
-          adequate testing, taking such environment variables as
-          parameters.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.firefox.syncserver</literal> now runs by
-          default as a non-root user. To accommodate this change, the
-          default sqlite database location has also been changed.
-          Migration should work automatically. Refer to the description
-          of the options for more details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>compiz</literal> window manager and package was
-          removed. The system support had been broken for several years.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Touchpad support should now be enabled through
-          <literal>libinput</literal> as <literal>synaptics</literal> is
-          now deprecated. See the option
-          <literal>services.xserver.libinput.enable</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          grsecurity/PaX support has been dropped, following upstream's
-          decision to cease free support. See
-          <link xlink:href="https://grsecurity.net/passing_the_baton.php">
-          upstream's announcement</link> for more information. No
-          complete replacement for grsecurity/PaX is available
-          presently.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.mysql</literal> now has declarative
-          configuration of databases and users with the
-          <literal>ensureDatabases</literal> and
-          <literal>ensureUsers</literal> options.
-        </para>
-        <para>
-          These options will never delete existing databases and users,
-          especially not when the value of the options are changed.
-        </para>
-        <para>
-          The MySQL users will be identified using
-          <link xlink:href="https://mariadb.com/kb/en/library/authentication-plugin-unix-socket/">
-          Unix socket authentication</link>. This authenticates the Unix
-          user with the same name only, and that without the need for a
-          password.
-        </para>
-        <para>
-          If you have previously created a MySQL <literal>root</literal>
-          user <emphasis>with a password</emphasis>, you will need to
-          add <literal>root</literal> user for unix socket
-          authentication before using the new options. This can be done
-          by running the following SQL script:
-        </para>
-        <programlisting language="SQL">
-CREATE USER 'root'@'%' IDENTIFIED BY '';
-GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
-FLUSH PRIVILEGES;
-
--- Optionally, delete the password-authenticated user:
--- DROP USER 'root'@'localhost';
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.mysqlBackup</literal> now works by default
-          without any user setup, including for users other than
-          <literal>mysql</literal>.
-        </para>
-        <para>
-          By default, the <literal>mysql</literal> user is no longer the
-          user which performs the backup. Instead a system account
-          <literal>mysqlbackup</literal> is used.
-        </para>
-        <para>
-          The <literal>mysqlBackup</literal> service is also now using
-          systemd timers instead of <literal>cron</literal>.
-        </para>
-        <para>
-          Therefore, the <literal>services.mysqlBackup.period</literal>
-          option no longer exists, and has been replaced with
-          <literal>services.mysqlBackup.calendar</literal>, which is in
-          the format of
-          <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.time.html#Calendar%20Events">systemd.time(7)</link>.
-        </para>
-        <para>
-          If you expect to be sent an e-mail when the backup fails,
-          consider using a script which monitors the systemd journal for
-          errors. Regretfully, at present there is no built-in
-          functionality for this.
-        </para>
-        <para>
-          You can check that backups still work by running
-          <literal>systemctl start mysql-backup</literal> then
-          <literal>systemctl status mysql-backup</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Templated systemd services e.g
-          <literal>container@name</literal> are now handled currectly
-          when switching to a new configuration, resulting in them being
-          reloaded.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Steam: the <literal>newStdcpp</literal> parameter was removed
-          and should not be needed anymore.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Redis has been updated to version 4 which mandates a cluster
-          mass-restart, due to changes in the network handling, in order
-          to ensure compatibility with networks NATing traffic.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-17.09-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Modules can now be disabled by using
-          <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-replace-modules">
-          disabledModules</link>, allowing another to take it's place.
-          This can be used to import a set of modules from another
-          channel while keeping the rest of the system on a stable
-          release.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Updated to FreeType 2.7.1, including a new TrueType engine.
-          The new engine replaces the Infinality engine which was the
-          default in NixOS. The default font rendering settings are now
-          provided by fontconfig-penultimate, replacing
-          fontconfig-ultimate; the new defaults are less invasive and
-          provide rendering that is more consistent with other systems
-          and hopefully with each font designer's intent. Some
-          system-wide configuration has been removed from the Fontconfig
-          NixOS module where user Fontconfig settings are available.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          ZFS/SPL have been updated to 0.7.0,
-          <literal>zfsUnstable, splUnstable</literal> have therefore
-          been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>time.timeZone</literal> option now allows the
-          value <literal>null</literal> in addition to timezone strings.
-          This value allows changing the timezone of a system
-          imperatively using
-          <literal>timedatectl set-timezone</literal>. The default
-          timezone is still UTC.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Nixpkgs overlays may now be specified with a file as well as a
-          directory. The value of
-          <literal>&lt;nixpkgs-overlays&gt;</literal> may be a file, and
-          <literal>~/.config/nixpkgs/overlays.nix</literal> can be used
-          instead of the <literal>~/.config/nixpkgs/overlays</literal>
-          directory.
-        </para>
-        <para>
-          See the overlays chapter of the Nixpkgs manual for more
-          details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Definitions for <literal>/etc/hosts</literal> can now be
-          specified declaratively with
-          <literal>networking.hosts</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Two new options have been added to the installer loader, in
-          addition to the default having changed. The kernel log
-          verbosity has been lowered to the upstream default for the
-          default options, in order to not spam the console when e.g.
-          joining a network.
-        </para>
-        <para>
-          This therefore leads to adding a new <literal>debug</literal>
-          option to set the log level to the previous verbose mode, to
-          make debugging easier, but still accessible easily.
-        </para>
-        <para>
-          Additionally a <literal>copytoram</literal> option has been
-          added, which makes it possible to remove the install medium
-          after booting. This allows tethering from your phone after
-          booting from it.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.gitlab-runner.configOptions</literal> has
-          been added to specify the configuration of gitlab-runners
-          declaratively.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.jenkins.plugins</literal> has been added to
-          install plugins easily, this can be generated with
-          jenkinsPlugins2nix.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.postfix.config</literal> has been added to
-          specify the main.cf with NixOS options. Additionally other
-          options have been added to the postfix module and has been
-          improved further.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The GitLab package and module have been updated to the latest
-          10.0 release.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>systemd-boot</literal> boot loader now lists the
-          NixOS version, kernel version and build date of all bootable
-          generations.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The dnscrypt-proxy service now defaults to using a random
-          upstream resolver, selected from the list of public
-          non-logging resolvers with DNSSEC support. Existing
-          configurations can be migrated to this mode of operation by
-          omitting the
-          <literal>services.dnscrypt-proxy.resolverName</literal> option
-          or setting it to <literal>&quot;random&quot;</literal>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</section>
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
deleted file mode 100644
index 910cad467e9..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1803.section.xml
+++ /dev/null
@@ -1,879 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-18.03">
-  <title>Release 18.03 (<quote>Impala</quote>, 2018/04/04)</title>
-  <section xml:id="sec-release-18.03-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      has the following highlights:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          End of support is planned for end of October 2018, handing
-          over to 18.09.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Platform support: x86_64-linux and x86_64-darwin since release
-          time (the latter isn't NixOS, really). Binaries for
-          aarch64-linux are available, but no channel exists yet, as
-          it's waiting for some test fixes, etc.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Nix now defaults to 2.0; see its
-          <link xlink:href="https://nixos.org/nix/manual/#ssec-relnotes-2.0">release
-          notes</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Core version changes: linux: 4.9 -&gt; 4.14, glibc: 2.25 -&gt;
-          2.26, gcc: 6 -&gt; 7, systemd: 234 -&gt; 237.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Desktop version changes: gnome: 3.24 -&gt; 3.26, (KDE)
-          plasma-desktop: 5.10 -&gt; 5.12.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          MariaDB 10.2, updated from 10.1, is now the default MySQL
-          implementation. While upgrading a few changes have been made
-          to the infrastructure involved:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>libmysql</literal> has been deprecated, please
-              use <literal>mysql.connector-c</literal> instead, a
-              compatibility passthru has been added to the MySQL
-              packages.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The <literal>mysql57</literal> package has a new
-              <literal>static</literal> output containing the static
-              libraries including <literal>libmysqld.a</literal>
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          PHP now defaults to PHP 7.2, updated from 7.1.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-18.03-new-services">
-    <title>New Services</title>
-    <para>
-      The following new services were added since the last release:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>./config/krb5/default.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./hardware/digitalbitbox.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./misc/label.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/ccache.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/criu.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/digitalbitbox/default.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/less.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/npm.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/plotinus.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/rootston.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/systemtap.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/sway.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/udevil.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/way-cooler.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/yabar.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/zsh/zsh-autoenv.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/backup/borgbackup.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/backup/crashplan-small-business.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/desktops/dleyna-renderer.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/desktops/dleyna-server.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/desktops/pipewire.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/desktops/gnome3/chrome-gnome-shell.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/desktops/gnome3/tracker-miners.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/hardware/fwupd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/hardware/interception-tools.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/hardware/u2f.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/hardware/usbmuxd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/mail/clamsmtp.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/mail/dkimproxy-out.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/mail/pfix-srsd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/gitea.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/home-assistant.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/ihaskell.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/logkeys.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/novacomd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/osrm.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/plexpy.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/pykms.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/tzupdate.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/monitoring/fusion-inventory.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/monitoring/prometheus/exporters.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/network-filesystems/beegfs.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/network-filesystems/davfs2.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/network-filesystems/openafs/client.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/network-filesystems/openafs/server.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/network-filesystems/ceph.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/aria2.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/monero.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/nghttpx/default.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/nixops-dns.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/rxe.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/stunnel.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/matomo.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/restya-board.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-servers/mighttpd2.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/x11/fractalart.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./system/boot/binfmt.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./system/boot/grow-partition.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./tasks/filesystems/ecryptfs.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./virtualisation/hyperv-guest.nix</literal>
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-18.03-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <para>
-      When upgrading from a previous release, please be aware of the
-      following incompatible changes:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>sound.enable</literal> now defaults to false.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Dollar signs in options under
-          <literal>services.postfix</literal> are passed verbatim to
-          Postfix, which will interpret them as the beginning of a
-          parameter expression. This was already true for string-valued
-          options in the previous release, but not for list-valued
-          options. If you need to pass literal dollar signs through
-          Postfix, double them.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>postage</literal> package (for web-based
-          PostgreSQL administration) has been renamed to
-          <literal>pgmanage</literal>. The corresponding module has also
-          been renamed. To migrate please rename all
-          <literal>services.postage</literal> options to
-          <literal>services.pgmanage</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Package attributes starting with a digit have been prefixed
-          with an underscore sign. This is to avoid quoting in the
-          configuration and other issues with command-line tools like
-          <literal>nix-env</literal>. The change affects the following
-          packages:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>2048-in-terminal</literal> →
-              <literal>_2048-in-terminal</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>90secondportraits</literal> →
-              <literal>_90secondportraits</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>2bwm</literal> → <literal>_2bwm</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>389-ds-base</literal> →
-              <literal>_389-ds-base</literal>
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          <emphasis role="strong">The OpenSSH service no longer enables
-          support for DSA keys by default, which could cause a system
-          lock out. Update your keys or, unfavorably, re-enable DSA
-          support manually.</emphasis>
-        </para>
-        <para>
-          DSA support was
-          <link xlink:href="https://www.openssh.com/legacy.html">deprecated
-          in OpenSSH 7.0</link>, due to it being too weak. To re-enable
-          support, add
-          <literal>PubkeyAcceptedKeyTypes +ssh-dss</literal> to the end
-          of your <literal>services.openssh.extraConfig</literal>.
-        </para>
-        <para>
-          After updating the keys to be stronger, anyone still on a
-          pre-17.03 version is safe to jump to 17.03, as vetted
-          <link xlink:href="https://search.nix.gsc.io/?q=stateVersion">here</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>openssh</literal> package now includes Kerberos
-          support by default; the
-          <literal>openssh_with_kerberos</literal> package is now a
-          deprecated alias. If you do not want Kerberos support, you can
-          do
-          <literal>openssh.override { withKerberos = false; }</literal>.
-          Note, this also applies to the <literal>openssh_hpn</literal>
-          package.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>cc-wrapper</literal> has been split in two; there is
-          now also a <literal>bintools-wrapper</literal>. The most
-          commonly used files in <literal>nix-support</literal> are now
-          split between the two wrappers. Some commonly used ones, like
-          <literal>nix-support/dynamic-linker</literal>, are duplicated
-          for backwards compatability, even though they rightly belong
-          only in <literal>bintools-wrapper</literal>. Other more
-          obscure ones are just moved.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The propagation logic has been changed. The new logic, along
-          with new types of dependencies that go with, is thoroughly
-          documented in the &quot;Specifying dependencies&quot; section
-          of the &quot;Standard Environment&quot; chapter of the nixpkgs
-          manual. The old logic isn't but is easy to describe:
-          dependencies were propagated as the same type of dependency no
-          matter what. In practice, that means that many
-          <literal>propagatedNativeBuildInputs</literal> should instead
-          be <literal>propagatedBuildInputs</literal>. Thankfully, that
-          was and is the least used type of dependency. Also, it means
-          that some <literal>propagatedBuildInputs</literal> should
-          instead be <literal>depsTargetTargetPropagated</literal>.
-          Other types dependencies should be unaffected.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.addPassthru drv passthru</literal> is removed.
-          Use <literal>lib.extendDerivation true passthru drv</literal>
-          instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>memcached</literal> service no longer accept
-          dynamic socket paths via
-          <literal>services.memcached.socket</literal>. Unix sockets can
-          be still enabled by
-          <literal>services.memcached.enableUnixSocket</literal> and
-          will be accessible at
-          <literal>/run/memcached/memcached.sock</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>hardware.amdHybridGraphics.disable</literal>
-          option was removed for lack of a maintainer. If you still need
-          this module, you may wish to include a copy of it from an
-          older version of nixos in your imports.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The merging of config options for
-          <literal>services.postfix.config</literal> was buggy.
-          Previously, if other options in the Postfix module like
-          <literal>services.postfix.useSrs</literal> were set and the
-          user set config options that were also set by such options,
-          the resulting config wouldn't include all options that were
-          needed. They are now merged correctly. If config options need
-          to be overridden, <literal>lib.mkForce</literal> or
-          <literal>lib.mkOverride</literal> can be used.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The following changes apply if the
-          <literal>stateVersion</literal> is changed to 18.03 or higher.
-          For <literal>stateVersion = &quot;17.09&quot;</literal> or
-          lower the old behavior is preserved.
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              <literal>matrix-synapse</literal> uses postgresql by
-              default instead of sqlite. Migration instructions can be
-              found
-              <link xlink:href="https://github.com/matrix-org/synapse/blob/master/docs/postgres.rst#porting-from-sqlite">
-              here </link>.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>jid</literal> package has been removed, due to
-          maintenance overhead of a go package having non-versioned
-          dependencies.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          When using <literal>services.xserver.libinput</literal>
-          (enabled by default in GNOME), it now handles all input
-          devices, not just touchpads. As a result, you might need to
-          re-evaluate any custom Xorg configuration. In particular,
-          <literal>Option &quot;XkbRules&quot; &quot;base&quot;</literal>
-          may result in broken keyboard layout.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>attic</literal> package was removed. A maintained
-          fork called
-          <link xlink:href="https://www.borgbackup.org/">Borg</link>
-          should be used instead. Migration instructions can be found
-          <link xlink:href="http://borgbackup.readthedocs.io/en/stable/usage/upgrade.html#attic-and-borg-0-xx-to-borg-1-x">here</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Piwik analytics software was renamed to Matomo:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              The package <literal>pkgs.piwik</literal> was renamed to
-              <literal>pkgs.matomo</literal>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The service <literal>services.piwik</literal> was renamed
-              to <literal>services.matomo</literal>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The data directory <literal>/var/lib/piwik</literal> was
-              renamed to <literal>/var/lib/matomo</literal>. All files
-              will be moved automatically on first startup, but you
-              might need to adjust your backup scripts.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The default <literal>serverName</literal> for the nginx
-              configuration changed from
-              <literal>piwik.${config.networking.hostName}</literal> to
-              <literal>matomo.${config.networking.hostName}.${config.networking.domain}</literal>
-              if <literal>config.networking.domain</literal> is set,
-              <literal>matomo.${config.networking.hostName}</literal> if
-              it is not set. If you change your
-              <literal>serverName</literal>, remember you'll need to
-              update the <literal>trustedHosts[]</literal> array in
-              <literal>/var/lib/matomo/config/config.ini.php</literal>
-              as well.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The <literal>piwik</literal> user was renamed to
-              <literal>matomo</literal>. The service will adjust
-              ownership automatically for files in the data directory.
-              If you use unix socket authentication, remember to give
-              the new <literal>matomo</literal> user access to the
-              database and to change the <literal>username</literal> to
-              <literal>matomo</literal> in the
-              <literal>[database]</literal> section of
-              <literal>/var/lib/matomo/config/config.ini.php</literal>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              If you named your database `piwik`, you might want to
-              rename it to `matomo` to keep things clean, but this is
-              neither enforced nor required.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>nodejs-4_x</literal> is end-of-life.
-          <literal>nodejs-4_x</literal>,
-          <literal>nodejs-slim-4_x</literal> and
-          <literal>nodePackages_4_x</literal> are removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>pump.io</literal> NixOS module was removed. It is
-          now maintained as an
-          <link xlink:href="https://github.com/rvl/pump.io-nixos">external
-          module</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Prosody XMPP server has received a major update. The
-          following modules were renamed:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>services.prosody.modules.httpserver</literal> is
-              now <literal>services.prosody.modules.http_files</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.prosody.modules.console</literal> is now
-              <literal>services.prosody.modules.admin_telnet</literal>
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          Many new modules are now core modules, most notably
-          <literal>services.prosody.modules.carbons</literal> and
-          <literal>services.prosody.modules.mam</literal>.
-        </para>
-        <para>
-          The better-performing <literal>libevent</literal> backend is
-          now enabled by default.
-        </para>
-        <para>
-          <literal>withCommunityModules</literal> now passes through the
-          modules to <literal>services.prosody.extraModules</literal>.
-          Use <literal>withOnlyInstalledCommunityModules</literal> for
-          modules that should not be enabled directly, e.g
-          <literal>lib_ldap</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          All prometheus exporter modules are now defined as submodules.
-          The exporters are configured using
-          <literal>services.prometheus.exporters</literal>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-18.03-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          ZNC option <literal>services.znc.mutable</literal> now
-          defaults to <literal>true</literal>. That means that old
-          configuration is not overwritten by default when update to the
-          znc options are made.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <literal>networking.wireless.networks.&lt;name&gt;.auth</literal>
-          has been added for wireless networks with WPA-Enterprise
-          authentication. There is also a new
-          <literal>extraConfig</literal> option to directly configure
-          <literal>wpa_supplicant</literal> and
-          <literal>hidden</literal> to connect to hidden networks.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          In the module
-          <literal>networking.interfaces.&lt;name&gt;</literal> the
-          following options have been removed:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>ipAddress</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>ipv6Address</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>prefixLength</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>ipv6PrefixLength</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>subnetMask</literal>
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          To assign static addresses to an interface the options
-          <literal>ipv4.addresses</literal> and
-          <literal>ipv6.addresses</literal> should be used instead. The
-          options <literal>ip4</literal> and <literal>ip6</literal> have
-          been renamed to <literal>ipv4.addresses</literal>
-          <literal>ipv6.addresses</literal> respectively. The new
-          options <literal>ipv4.routes</literal> and
-          <literal>ipv6.routes</literal> have been added to set up
-          static routing.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option <literal>services.logstash.listenAddress</literal>
-          is now <literal>127.0.0.1</literal> by default. Previously the
-          default behaviour was to listen on all interfaces.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.btrfs.autoScrub</literal> has been added, to
-          periodically check btrfs filesystems for data corruption. If
-          there's a correct copy available, it will automatically repair
-          corrupted blocks.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>displayManager.lightdm.greeters.gtk.clock-format.</literal>
-          has been added, the clock format string (as expected by
-          strftime, e.g. <literal>%H:%M</literal>) to use with the
-          lightdm gtk greeter panel.
-        </para>
-        <para>
-          If set to null the default clock format is used.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>displayManager.lightdm.greeters.gtk.indicators</literal>
-          has been added, a list of allowed indicator modules to use
-          with the lightdm gtk greeter panel.
-        </para>
-        <para>
-          Built-in indicators include <literal>~a11y</literal>,
-          <literal>~language</literal>, <literal>~session</literal>,
-          <literal>~power</literal>, <literal>~clock</literal>,
-          <literal>~host</literal>, <literal>~spacer</literal>. Unity
-          indicators can be represented by short name (e.g.
-          <literal>sound</literal>, <literal>power</literal>), service
-          file name, or absolute path.
-        </para>
-        <para>
-          If set to <literal>null</literal> the default indicators are
-          used.
-        </para>
-        <para>
-          In order to have the previous default configuration add
-        </para>
-        <programlisting language="bash">
-{
-  services.xserver.displayManager.lightdm.greeters.gtk.indicators = [
-    &quot;~host&quot; &quot;~spacer&quot;
-    &quot;~clock&quot; &quot;~spacer&quot;
-    &quot;~session&quot;
-    &quot;~language&quot;
-    &quot;~a11y&quot;
-    &quot;~power&quot;
-  ];
-}
-</programlisting>
-        <para>
-          to your <literal>configuration.nix</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The NixOS test driver supports user services declared by
-          <literal>systemd.user.services</literal>. The methods
-          <literal>waitForUnit</literal>,
-          <literal>getUnitInfo</literal>, <literal>startJob</literal>
-          and <literal>stopJob</literal> provide an optional
-          <literal>$user</literal> argument for that purpose.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Enabling bash completion on NixOS,
-          <literal>programs.bash.enableCompletion</literal>, will now
-          also enable completion for the Nix command line tools by
-          installing the
-          <link xlink:href="https://github.com/hedning/nix-bash-completions">nix-bash-completions</link>
-          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-1809.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1809.section.xml
deleted file mode 100644
index aa4637a99b6..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1809.section.xml
+++ /dev/null
@@ -1,941 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-18.09">
-  <title>Release 18.09 (<quote>Jellyfish</quote>, 2018/10/05)</title>
-  <section xml:id="sec-release-18.09-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      has the following notable updates:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          End of support is planned for end of April 2019, handing over
-          to 19.03.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Platform support: x86_64-linux and x86_64-darwin as always.
-          Support for aarch64-linux is as with the previous releases,
-          not equivalent to the x86-64-linux release, but with efforts
-          to reach parity.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Nix has been updated to 2.1; see its
-          <link xlink:href="https://nixos.org/nix/manual/#ssec-relnotes-2.1">release
-          notes</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Core versions: linux: 4.14 LTS (unchanged), glibc: 2.26 →
-          2.27, gcc: 7 (unchanged), systemd: 237 → 239.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Desktop version changes: gnome: 3.26 → 3.28, (KDE)
-          plasma-desktop: 5.12 → 5.13.
-        </para>
-      </listitem>
-    </itemizedlist>
-    <para>
-      Notable changes and additions for 18.09 include:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Support for wrapping binaries using
-          <literal>firejail</literal> has been added through
-          <literal>programs.firejail.wrappedBinaries</literal>.
-        </para>
-        <para>
-          For example
-        </para>
-        <programlisting language="bash">
-{
-  programs.firejail = {
-    enable = true;
-    wrappedBinaries = {
-      firefox = &quot;${lib.getBin pkgs.firefox}/bin/firefox&quot;;
-      mpv = &quot;${lib.getBin pkgs.mpv}/bin/mpv&quot;;
-    };
-  };
-}
-</programlisting>
-        <para>
-          This will place <literal>firefox</literal> and
-          <literal>mpv</literal> binaries in the global path wrapped by
-          firejail.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          User channels are now in the default
-          <literal>NIX_PATH</literal>, allowing users to use their
-          personal <literal>nix-channel</literal> defined channels in
-          <literal>nix-build</literal> and <literal>nix-shell</literal>
-          commands, as well as in imports like
-          <literal>import &lt;mychannel&gt;</literal>.
-        </para>
-        <para>
-          For example
-        </para>
-        <programlisting>
-$ nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgsunstable
-$ nix-channel --update
-$ nix-build '&lt;nixpkgsunstable&gt;' -A gitFull
-$ nix run -f '&lt;nixpkgsunstable&gt;' gitFull
-$ nix-instantiate -E '(import &lt;nixpkgsunstable&gt; {}).gitFull'
-</programlisting>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-18.09-new-services">
-    <title>New Services</title>
-    <para>
-      A curated selection of new services that were added since the last
-      release:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The <literal>services.cassandra</literal> module has been
-          reworked and was rewritten from scratch. The service has
-          succeeding tests for the versions 2.1, 2.2, 3.0 and 3.11 of
-          <link xlink:href="https://cassandra.apache.org/">Apache
-          Cassandra</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          There is a new <literal>services.foundationdb</literal> module
-          for deploying
-          <link xlink:href="https://www.foundationdb.org">FoundationDB</link>
-          clusters.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          When enabled the <literal>iproute2</literal> will copy the
-          files expected by ip route (e.g.,
-          <literal>rt_tables</literal>) in
-          <literal>/etc/iproute2</literal>. This allows to write aliases
-          for routing tables for instance.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.strongswan-swanctl</literal> is a modern
-          replacement for <literal>services.strongswan</literal>. You
-          can use either one of them to setup IPsec VPNs but not both at
-          the same time.
-        </para>
-        <para>
-          <literal>services.strongswan-swanctl</literal> uses the
-          <link xlink:href="https://wiki.strongswan.org/projects/strongswan/wiki/swanctl">swanctl</link>
-          command which uses the modern
-          <link xlink:href="https://github.com/strongswan/strongswan/blob/master/src/libcharon/plugins/vici/README.md">vici</link>
-          <emphasis>Versatile IKE Configuration Interface</emphasis>.
-          The deprecated <literal>ipsec</literal> command used in
-          <literal>services.strongswan</literal> is using the legacy
-          <link xlink:href="https://github.com/strongswan/strongswan/blob/master/README_LEGACY.md">stroke
-          configuration interface</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The new <literal>services.elasticsearch-curator</literal>
-          service periodically curates or manages, your Elasticsearch
-          indices and snapshots.
-        </para>
-      </listitem>
-    </itemizedlist>
-    <para>
-      Every new services:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>./config/xdg/autostart.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./config/xdg/icons.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./config/xdg/menus.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./config/xdg/mime.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./hardware/brightnessctl.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./hardware/onlykey.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./hardware/video/uvcvideo/default.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./misc/documentation.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/firejail.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/iftop.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/sedutil.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/singularity.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/xss-lock.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/zsh/zsh-autosuggestions.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/admin/oxidized.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/backup/duplicati.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/backup/restic.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/backup/restic-rest-server.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/cluster/hadoop/default.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/databases/aerospike.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/databases/monetdb.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/desktops/bamf.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/desktops/flatpak.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/desktops/zeitgeist.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/development/bloop.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/development/jupyter/default.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/hardware/lcd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/hardware/undervolt.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/clipmenu.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/gitweb.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/serviio.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/safeeyes.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/sysprof.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/weechat.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/monitoring/datadog-agent.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/monitoring/incron.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/dnsdist.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/freeradius.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/hans.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/morty.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/ndppd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/ocserv.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/owamp.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/quagga.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/shadowsocks.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/stubby.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/zeronet.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/security/certmgr.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/security/cfssl.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/security/oauth2_proxy_nginx.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/virtlyst.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/youtrack.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-servers/hitch/default.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-servers/hydron.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-servers/meguca.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-servers/nginx/gitweb.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./virtualisation/kvmgt.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./virtualisation/qemu-guest-agent.nix</literal>
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-18.09-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <para>
-      When upgrading from a previous release, please be aware of the
-      following incompatible changes:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Some licenses that were incorrectly not marked as unfree now
-          are. This is the case for:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              cc-by-nc-sa-20: Creative Commons Attribution Non
-              Commercial Share Alike 2.0
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              cc-by-nc-sa-25: Creative Commons Attribution Non
-              Commercial Share Alike 2.5
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              cc-by-nc-sa-30: Creative Commons Attribution Non
-              Commercial Share Alike 3.0
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              cc-by-nc-sa-40: Creative Commons Attribution Non
-              Commercial Share Alike 4.0
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              cc-by-nd-30: Creative Commons Attribution-No Derivative
-              Works v3.00
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              msrla: Microsoft Research License Agreement
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The deprecated <literal>services.cassandra</literal> module
-          has seen a complete rewrite. (See above.)
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.strict</literal> is removed. Use
-          <literal>builtins.seq</literal> instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>clementine</literal> package points now to the
-          free derivation. <literal>clementineFree</literal> is removed
-          now and <literal>clementineUnfree</literal> points to the
-          package which is bundled with the unfree
-          <literal>libspotify</literal> package.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>netcat</literal> package is now taken directly
-          from OpenBSD's <literal>libressl</literal>, instead of relying
-          on Debian's fork. The new version should be very close to the
-          old version, but there are some minor differences.
-          Importantly, flags like -b, -q, -C, and -Z are no longer
-          accepted by the nc command.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.docker-registry.extraConfig</literal>
-          object doesn't contain environment variables anymore. Instead
-          it needs to provide an object structure that can be mapped
-          onto the YAML configuration defined in
-          <link xlink:href="https://github.com/docker/distribution/blob/v2.6.2/docs/configuration.md">the
-          <literal>docker/distribution</literal> docs</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>gnucash</literal> has changed from version 2.4 to
-          3.x. If you've been using <literal>gnucash</literal> (version
-          2.4) instead of <literal>gnucash26</literal> (version 2.6) you
-          must open your Gnucash data file(s) with
-          <literal>gnucash26</literal> and then save them to upgrade the
-          file format. Then you may use your data file(s) with Gnucash
-          3.x. See the upgrade
-          <link xlink:href="https://wiki.gnucash.org/wiki/FAQ#Using_Different_Versions.2C_Up_And_Downgrade">documentation</link>.
-          Gnucash 2.4 is still available under the attribute
-          <literal>gnucash24</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.munge</literal> now runs as user (and group)
-          <literal>munge</literal> instead of root. Make sure the key
-          file is accessible to the daemon.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>dockerTools.buildImage</literal> now uses
-          <literal>null</literal> as default value for
-          <literal>tag</literal>, which indicates that the nix output
-          hash will be used as tag.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The ELK stack: <literal>elasticsearch</literal>,
-          <literal>logstash</literal> and <literal>kibana</literal> has
-          been upgraded from 2.* to 6.3.*. The 2.* versions have been
-          <link xlink:href="https://www.elastic.co/support/eol">unsupported
-          since last year</link> so they have been removed. You can
-          still use the 5.* versions under the names
-          <literal>elasticsearch5</literal>,
-          <literal>logstash5</literal> and <literal>kibana5</literal>.
-        </para>
-        <para>
-          The elastic beats: <literal>filebeat</literal>,
-          <literal>heartbeat</literal>, <literal>metricbeat</literal>
-          and <literal>packetbeat</literal> have had the same treatment:
-          they now target 6.3.* as well. The 5.* versions are available
-          under the names: <literal>filebeat5</literal>,
-          <literal>heartbeat5</literal>, <literal>metricbeat5</literal>
-          and <literal>packetbeat5</literal>
-        </para>
-        <para>
-          The ELK-6.3 stack now comes with
-          <link xlink:href="https://www.elastic.co/products/x-pack/open">X-Pack
-          by default</link>. Since X-Pack is licensed under the
-          <link xlink:href="https://github.com/elastic/elasticsearch/blob/master/licenses/ELASTIC-LICENSE.txt">Elastic
-          License</link> the ELK packages now have an unfree license. To
-          use them you need to specify
-          <literal>allowUnfree = true;</literal> in your nixpkgs
-          configuration.
-        </para>
-        <para>
-          Fortunately there is also a free variant of the ELK stack
-          without X-Pack. The packages are available under the names:
-          <literal>elasticsearch-oss</literal>,
-          <literal>logstash-oss</literal> and
-          <literal>kibana-oss</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Options
-          <literal>boot.initrd.luks.devices.name.yubikey.ramfsMountPoint</literal>
-          <literal>boot.initrd.luks.devices.name.yubikey.storage.mountPoint</literal>
-          were removed. <literal>luksroot.nix</literal> module never
-          supported more than one YubiKey at a time anyway, hence those
-          options never had any effect. You should be able to remove
-          them from your config without any issues.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>stdenv.system</literal> and <literal>system</literal>
-          in nixpkgs now refer to the host platform instead of the build
-          platform. For native builds this is not change, let alone a
-          breaking one. For cross builds, it is a breaking change, and
-          <literal>stdenv.buildPlatform.system</literal> can be used
-          instead for the old behavior. They should be using that
-          anyways for clarity.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Groups <literal>kvm</literal> and <literal>render</literal>
-          are introduced now, as systemd requires them.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-18.09-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>dockerTools.pullImage</literal> relies on image
-          digest instead of image tag to download the image. The
-          <literal>sha256</literal> of a pulled image has to be updated.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.attrNamesToStr</literal> has been deprecated. Use
-          more specific concatenation
-          (<literal>lib.concat(Map)StringsSep</literal>) instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.addErrorContextToAttrs</literal> has been
-          deprecated. Use <literal>builtins.addErrorContext</literal>
-          directly.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.showVal</literal> has been deprecated. Use
-          <literal>lib.traceSeqN</literal> instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.traceXMLVal</literal> has been deprecated. Use
-          <literal>lib.traceValFn builtins.toXml</literal> instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.traceXMLValMarked</literal> has been deprecated.
-          Use
-          <literal>lib.traceValFn (x: str + builtins.toXML x)</literal>
-          instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>pkgs</literal> argument to NixOS modules can now
-          be set directly using <literal>nixpkgs.pkgs</literal>.
-          Previously, only the <literal>system</literal>,
-          <literal>config</literal> and <literal>overlays</literal>
-          arguments could be used to influence <literal>pkgs</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          A NixOS system can now be constructed more easily based on a
-          preexisting invocation of Nixpkgs. For example:
-        </para>
-        <programlisting language="bash">
-{
-  inherit (pkgs.nixos {
-    boot.loader.grub.enable = false;
-    fileSystems.&quot;/&quot;.device = &quot;/dev/xvda1&quot;;
-  }) toplevel kernel initialRamdisk manual;
-}
-</programlisting>
-        <para>
-          This benefits evaluation performance, lets you write Nixpkgs
-          packages that depend on NixOS images and is consistent with a
-          deployment architecture that would be centered around Nixpkgs
-          overlays.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.traceValIfNot</literal> has been deprecated. Use
-          <literal>if/then/else</literal> and
-          <literal>lib.traceValSeq</literal> instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.traceCallXml</literal> has been deprecated.
-          Please complain if you use the function regularly.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The attribute <literal>lib.nixpkgsVersion</literal> has been
-          deprecated in favor of <literal>lib.version</literal>. Please
-          refer to the discussion in
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/39416#discussion_r183845745">NixOS/nixpkgs#39416</link>
-          for further reference.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.recursiveUpdateUntil</literal> was not acting
-          according to its specification. It has been fixed to act
-          according to the docstring, and a test has been added.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The module for <literal>security.dhparams</literal> has two
-          new options now:
-        </para>
-        <variablelist>
-          <varlistentry>
-            <term>
-              <literal>security.dhparams.stateless</literal>
-            </term>
-            <listitem>
-              <para>
-                Puts the generated Diffie-Hellman parameters into the
-                Nix store instead of managing them in a stateful manner
-                in <literal>/var/lib/dhparams</literal>.
-              </para>
-            </listitem>
-          </varlistentry>
-          <varlistentry>
-            <term>
-              <literal>security.dhparams.defaultBitSize</literal>
-            </term>
-            <listitem>
-              <para>
-                The default bit size to use for the generated
-                Diffie-Hellman parameters.
-              </para>
-            </listitem>
-          </varlistentry>
-        </variablelist>
-        <note>
-          <para>
-            The path to the actual generated parameter files should now
-            be queried using
-            <literal>config.security.dhparams.params.name.path</literal>
-            because it might be either in the Nix store or in a
-            directory configured by
-            <literal>security.dhparams.path</literal>.
-          </para>
-        </note>
-        <note>
-          <para>
-            <emphasis role="strong">For developers:</emphasis>
-          </para>
-          <para>
-            Module implementers should not set a specific bit size in
-            order to let users configure it by themselves if they want
-            to have a different bit size than the default (2048).
-          </para>
-          <para>
-            An example usage of this would be:
-          </para>
-          <programlisting language="bash">
-{ config, ... }:
-
-{
-  security.dhparams.params.myservice = {};
-  environment.etc.&quot;myservice.conf&quot;.text = ''
-    dhparams = ${config.security.dhparams.params.myservice.path}
-  '';
-}
-</programlisting>
-        </note>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>networking.networkmanager.useDnsmasq</literal> has
-          been deprecated. Use
-          <literal>networking.networkmanager.dns</literal> instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Kubernetes package has been bumped to major version 1.11.
-          Please consult the
-          <link xlink:href="https://github.com/kubernetes/kubernetes/blob/release-1.11/CHANGELOG-1.11.md">release
-          notes</link> for details on new features and api changes.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <literal>services.kubernetes.apiserver.admissionControl</literal>
-          was renamed to
-          <literal>services.kubernetes.apiserver.enableAdmissionPlugins</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Recommended way to access the Kubernetes Dashboard is via
-          HTTPS (TLS) Therefore; public service port for the dashboard
-          has changed to 443 (container port 8443) and scheme to https.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <literal>services.kubernetes.apiserver.address</literal> was
-          renamed to
-          <literal>services.kubernetes.apiserver.bindAddress</literal>.
-          Note that the default value has changed from 127.0.0.1 to
-          0.0.0.0.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <literal>services.kubernetes.apiserver.publicAddress</literal>
-          was not used and thus has been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <literal>services.kubernetes.addons.dashboard.enableRBAC</literal>
-          was renamed to
-          <literal>services.kubernetes.addons.dashboard.rbac.enable</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Kubernetes Dashboard now has only minimal RBAC permissions
-          by default. If dashboard cluster-admin rights are desired, set
-          <literal>services.kubernetes.addons.dashboard.rbac.clusterAdmin</literal>
-          to true. On existing clusters, in order for the revocation of
-          privileges to take effect, the current ClusterRoleBinding for
-          kubernetes-dashboard must be manually removed:
-          <literal>kubectl delete clusterrolebinding kubernetes-dashboard</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>programs.screen</literal> module provides allows
-          to configure <literal>/etc/screenrc</literal>, however the
-          module behaved fairly counterintuitive as the config exists,
-          but the package wasn't available. Since 18.09
-          <literal>pkgs.screen</literal> will be added to
-          <literal>environment.systemPackages</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The module <literal>services.networking.hostapd</literal> now
-          uses WPA2 by default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>s6Dns</literal>, <literal>s6Networking</literal>,
-          <literal>s6LinuxUtils</literal> and
-          <literal>s6PortableUtils</literal> renamed to
-          <literal>s6-dns</literal>, <literal>s6-networking</literal>,
-          <literal>s6-linux-utils</literal> and
-          <literal>s6-portable-utils</literal> respectively.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The module option <literal>nix.useSandbox</literal> is now
-          defaulted to <literal>true</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The config activation script of
-          <literal>nixos-rebuild</literal> now
-          <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemctl.html#Manager%20Lifecycle%20Commands">reloads</link>
-          all user units for each authenticated user.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default display manager is now LightDM. To use SLiM set
-          <literal>services.xserver.displayManager.slim.enable</literal>
-          to <literal>true</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          NixOS option descriptions are now automatically broken up into
-          individual paragraphs if the text contains two consecutive
-          newlines, so it's no longer necessary to use
-          <literal>&lt;/para&gt;&lt;para&gt;</literal> to start a new
-          paragraph.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Top-level <literal>buildPlatform</literal>,
-          <literal>hostPlatform</literal>, and
-          <literal>targetPlatform</literal> in Nixpkgs are deprecated.
-          Please use their equivalents in <literal>stdenv</literal>
-          instead: <literal>stdenv.buildPlatform</literal>,
-          <literal>stdenv.hostPlatform</literal>, and
-          <literal>stdenv.targetPlatform</literal>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1903.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1903.section.xml
deleted file mode 100644
index 31c5c1fc7f4..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1903.section.xml
+++ /dev/null
@@ -1,790 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-19.03">
-  <title>Release 19.03 (<quote>Koi</quote>, 2019/04/11)</title>
-  <section xml:id="sec-release-19.03-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      has the following highlights:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          End of support is planned for end of October 2019, handing
-          over to 19.09.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default Python 3 interpreter is now CPython 3.7 instead of
-          CPython 3.6.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Added the Pantheon desktop environment. It can be enabled
-          through
-          <literal>services.xserver.desktopManager.pantheon.enable</literal>.
-        </para>
-        <note>
-          <para>
-            By default,
-            <literal>services.xserver.desktopManager.pantheon</literal>
-            enables LightDM as a display manager, as pantheon's screen
-            locking implementation relies on it. Because of that it is
-            recommended to leave LightDM enabled. If you'd like to
-            disable it anyway, set
-            <literal>services.xserver.displayManager.lightdm.enable</literal>
-            to <literal>false</literal> and enable your preferred
-            display manager.
-          </para>
-        </note>
-        <para>
-          Also note that Pantheon's LightDM greeter is not enabled by
-          default, because it has numerous issues in NixOS and isn't
-          optimal for use here yet.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          A major refactoring of the Kubernetes module has been
-          completed. Refactorings primarily focus on decoupling
-          components and enhancing security. Two-way TLS and RBAC has
-          been enabled by default for all components, which slightly
-          changes the way the module is configured. See:
-          <xref linkend="sec-kubernetes" /> for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          There is now a set of <literal>confinement</literal> options
-          for <literal>systemd.services</literal>, which allows to
-          restrict services into a chroot 2 ed environment that only
-          contains the store paths from the runtime closure of the
-          service.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-19.03-new-services">
-    <title>New Services</title>
-    <para>
-      The following new services were added since the last release:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>./programs/nm-applet.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          There is a new <literal>security.googleOsLogin</literal>
-          module for using
-          <link xlink:href="https://cloud.google.com/compute/docs/instances/managing-instance-access">OS
-          Login</link> to manage SSH access to Google Compute Engine
-          instances, which supersedes the imperative and broken
-          <literal>google-accounts-daemon</literal> used in
-          <literal>nixos/modules/virtualisation/google-compute-config.nix</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/beanstalkd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          There is a new <literal>services.cockroachdb</literal> module
-          for running CockroachDB databases. NixOS now ships with
-          CockroachDB 2.1.x as well, available on
-          <literal>x86_64-linux</literal> and
-          <literal>aarch64-linux</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./security/duosec.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <link xlink:href="https://duo.com/docs/duounix">PAM module
-          for Duo Security</link> has been enabled for use. One can
-          configure it using the <literal>security.duosec</literal>
-          options along with the corresponding PAM option in
-          <literal>security.pam.services.&lt;name?&gt;.duoSecurity.enable</literal>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-19.03-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <para>
-      When upgrading from a previous release, please be aware of the
-      following incompatible changes:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The minimum version of Nix required to evaluate Nixpkgs is now
-          2.0.
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              For users of NixOS 18.03 and 19.03, NixOS defaults to Nix
-              2.0, but supports using Nix 1.11 by setting
-              <literal>nix.package = pkgs.nix1;</literal>. If this
-              option is set to a Nix 1.11 package, you will need to
-              either unset the option or upgrade it to Nix 2.0.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              For users of NixOS 17.09, you will first need to upgrade
-              Nix by setting
-              <literal>nix.package = pkgs.nixStable2;</literal> and run
-              <literal>nixos-rebuild switch</literal> as the
-              <literal>root</literal> user.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              For users of a daemon-less Nix installation on Linux or
-              macOS, you can upgrade Nix by running
-              <literal>curl -L https://nixos.org/nix/install | sh</literal>,
-              or prior to doing a channel update, running
-              <literal>nix-env -iA nix</literal>. If you have already
-              run a channel update and Nix is no longer able to evaluate
-              Nixpkgs, the error message printed should provide adequate
-              directions for upgrading Nix.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              For users of the Nix daemon on macOS, you can upgrade Nix
-              by running
-              <literal>sudo -i sh -c 'nix-channel --update &amp;&amp; nix-env -iA nixpkgs.nix'; sudo launchctl stop org.nixos.nix-daemon; sudo launchctl start org.nixos.nix-daemon</literal>.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>buildPythonPackage</literal> function now sets
-          <literal>strictDeps = true</literal> to help distinguish
-          between native and non-native dependencies in order to improve
-          cross-compilation compatibility. Note however that this may
-          break user expressions.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>buildPythonPackage</literal> function now sets
-          <literal>LANG = C.UTF-8</literal> to enable Unicode support.
-          The <literal>glibcLocales</literal> package is no longer
-          needed as a build input.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Syncthing state and configuration data has been moved from
-          <literal>services.syncthing.dataDir</literal> to the newly
-          defined <literal>services.syncthing.configDir</literal>, which
-          default to
-          <literal>/var/lib/syncthing/.config/syncthing</literal>. This
-          change makes possible to share synced directories using ACLs
-          without Syncthing resetting the permission on every start.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>ntp</literal> module now has sane default
-          restrictions. If you're relying on the previous defaults,
-          which permitted all queries and commands from all
-          firewall-permitted sources, you can set
-          <literal>services.ntp.restrictDefault</literal> and
-          <literal>services.ntp.restrictSource</literal> to
-          <literal>[]</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Package <literal>rabbitmq_server</literal> is renamed to
-          <literal>rabbitmq-server</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>light</literal> module no longer uses setuid
-          binaries, but udev rules. As a consequence users of that
-          module have to belong to the <literal>video</literal> group in
-          order to use the executable (i.e.
-          <literal>users.users.yourusername.extraGroups = [&quot;video&quot;];</literal>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Buildbot now supports Python 3 and its packages have been
-          moved to <literal>pythonPackages</literal>. The options
-          <literal>services.buildbot-master.package</literal> and
-          <literal>services.buildbot-worker.package</literal> can be
-          used to select the Python 2 or 3 version of the package.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Options
-          <literal>services.znc.confOptions.networks.name.userName</literal>
-          and
-          <literal>services.znc.confOptions.networks.name.modulePackages</literal>
-          were removed. They were never used for anything and can
-          therefore safely be removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Package <literal>wasm</literal> has been renamed
-          <literal>proglodyte-wasm</literal>. The package
-          <literal>wasm</literal> will be pointed to
-          <literal>ocamlPackages.wasm</literal> in 19.09, so make sure
-          to update your configuration if you want to keep
-          <literal>proglodyte-wasm</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          When the <literal>nixpkgs.pkgs</literal> option is set, NixOS
-          will no longer ignore the <literal>nixpkgs.overlays</literal>
-          option. The old behavior can be recovered by setting
-          <literal>nixpkgs.overlays = lib.mkForce [];</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          OpenSMTPD has been upgraded to version 6.4.0p1. This release
-          makes backwards-incompatible changes to the configuration file
-          format. See <literal>man smtpd.conf</literal> for more
-          information on the new file format.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The versioned <literal>postgresql</literal> have been renamed
-          to use underscore number separators. For example,
-          <literal>postgresql96</literal> has been renamed to
-          <literal>postgresql_9_6</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Package <literal>consul-ui</literal> and passthrough
-          <literal>consul.ui</literal> have been removed. The package
-          <literal>consul</literal> now uses upstream releases that
-          vendor the UI into the binary. See
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/48714#issuecomment-433454834">#48714</link>
-          for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Slurm introduces the new option
-          <literal>services.slurm.stateSaveLocation</literal>, which is
-          now set to <literal>/var/spool/slurm</literal> by default
-          (instead of <literal>/var/spool</literal>). Make sure to move
-          all files to the new directory or to set the option
-          accordingly.
-        </para>
-        <para>
-          The slurmctld now runs as user <literal>slurm</literal>
-          instead of <literal>root</literal>. If you want to keep
-          slurmctld running as <literal>root</literal>, set
-          <literal>services.slurm.user = root</literal>.
-        </para>
-        <para>
-          The options <literal>services.slurm.nodeName</literal> and
-          <literal>services.slurm.partitionName</literal> are now sets
-          of strings to correctly reflect that fact that each of these
-          options can occour more than once in the configuration.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>solr</literal> package has been upgraded from
-          4.10.3 to 7.5.0 and has undergone some major changes. The
-          <literal>services.solr</literal> module has been updated to
-          reflect these changes. Please review
-          http://lucene.apache.org/solr/ carefully before upgrading.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Package <literal>ckb</literal> is renamed to
-          <literal>ckb-next</literal>, and options
-          <literal>hardware.ckb.*</literal> are renamed to
-          <literal>hardware.ckb-next.*</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <literal>services.xserver.displayManager.job.logToFile</literal>
-          which was previously set to <literal>true</literal> when using
-          the display managers <literal>lightdm</literal>,
-          <literal>sddm</literal> or <literal>xpra</literal> has been
-          reset to the default value (<literal>false</literal>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Network interface indiscriminate NixOS firewall options
-          (<literal>networking.firewall.allow*</literal>) are now
-          preserved when also setting interface specific rules such as
-          <literal>networking.firewall.interfaces.en0.allow*</literal>.
-          These rules continue to use the pseudo device
-          &quot;default&quot;
-          (<literal>networking.firewall.interfaces.default.*</literal>),
-          and assigning to this pseudo device will override the
-          (<literal>networking.firewall.allow*</literal>) options.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>nscd</literal> service now disables all caching
-          of <literal>passwd</literal> and <literal>group</literal>
-          databases by default. This was interferring with the correct
-          functioning of the <literal>libnss_systemd.so</literal> module
-          which is used by <literal>systemd</literal> to manage uids and
-          usernames in the presence of <literal>DynamicUser=</literal>
-          in systemd services. This was already the default behaviour in
-          presence of <literal>services.sssd.enable = true</literal>
-          because nscd caching would interfere with
-          <literal>sssd</literal> in unpredictable ways as well. Because
-          we're using nscd not for caching, but for convincing glibc to
-          find NSS modules in the nix store instead of an absolute path,
-          we have decided to disable caching globally now, as it's
-          usually not the behaviour the user wants and can lead to
-          surprising behaviour. Furthermore, negative caching of host
-          lookups is also disabled now by default. This should fix the
-          issue of dns lookups failing in the presence of an unreliable
-          network.
-        </para>
-        <para>
-          If the old behaviour is desired, this can be restored by
-          setting the <literal>services.nscd.config</literal> option
-          with the desired caching parameters.
-        </para>
-        <programlisting language="bash">
-{
-  services.nscd.config =
-  ''
-  server-user             nscd
-  threads                 1
-  paranoia                no
-  debug-level             0
-
-  enable-cache            passwd          yes
-  positive-time-to-live   passwd          600
-  negative-time-to-live   passwd          20
-  suggested-size          passwd          211
-  check-files             passwd          yes
-  persistent              passwd          no
-  shared                  passwd          yes
-
-  enable-cache            group           yes
-  positive-time-to-live   group           3600
-  negative-time-to-live   group           60
-  suggested-size          group           211
-  check-files             group           yes
-  persistent              group           no
-  shared                  group           yes
-
-  enable-cache            hosts           yes
-  positive-time-to-live   hosts           600
-  negative-time-to-live   hosts           5
-  suggested-size          hosts           211
-  check-files             hosts           yes
-  persistent              hosts           no
-  shared                  hosts           yes
-  '';
-}
-</programlisting>
-        <para>
-          See
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/50316">#50316</link>
-          for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          GitLab Shell previously used the nix store paths for the
-          <literal>gitlab-shell</literal> command in its
-          <literal>authorized_keys</literal> file, which might stop
-          working after garbage collection. To circumvent that, we
-          regenerated that file on each startup. As
-          <literal>gitlab-shell</literal> has now been changed to use
-          <literal>/var/run/current-system/sw/bin/gitlab-shell</literal>,
-          this is not necessary anymore, but there might be leftover
-          lines with a nix store path. Regenerate the
-          <literal>authorized_keys</literal> file via
-          <literal>sudo -u git -H gitlab-rake gitlab:shell:setup</literal>
-          in that case.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>pam_unix</literal> account module is now loaded
-          with its control field set to <literal>required</literal>
-          instead of <literal>sufficient</literal>, so that later PAM
-          account modules that might do more extensive checks are being
-          executed. Previously, the whole account module verification
-          was exited prematurely in case a nss module provided the
-          account name to <literal>pam_unix</literal>. The LDAP and SSSD
-          NixOS modules already add their NSS modules when enabled. In
-          case your setup breaks due to some later PAM account module
-          previosuly shadowed, or failing NSS lookups, please file a
-          bug. You can get back the old behaviour by manually setting
-          <literal>security.pam.services.&lt;name?&gt;.text</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>pam_unix</literal> password module is now loaded
-          with its control field set to <literal>sufficient</literal>
-          instead of <literal>required</literal>, so that password
-          managed only by later PAM password modules are being executed.
-          Previously, for example, changing an LDAP account's password
-          through PAM was not possible: the whole password module
-          verification was exited prematurely by
-          <literal>pam_unix</literal>, preventing
-          <literal>pam_ldap</literal> to manage the password as it
-          should.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>fish</literal> has been upgraded to 3.0. It comes
-          with a number of improvements and backwards incompatible
-          changes. See the <literal>fish</literal>
-          <link xlink:href="https://github.com/fish-shell/fish-shell/releases/tag/3.0.0">release
-          notes</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The ibus-table input method has had a change in config format,
-          which causes all previous settings to be lost. See
-          <link xlink:href="https://github.com/mike-fabian/ibus-table/commit/f9195f877c5212fef0dfa446acb328c45ba5852b">this
-          commit message</link> for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          NixOS module system type <literal>types.optionSet</literal>
-          and <literal>lib.mkOption</literal> argument
-          <literal>options</literal> are deprecated. Use
-          <literal>types.submodule</literal> instead.
-          (<link xlink:href="https://github.com/NixOS/nixpkgs/pull/54637">#54637</link>)
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>matrix-synapse</literal> has been updated to version
-          0.99. It will
-          <link xlink:href="https://github.com/matrix-org/synapse/pull/4509">no
-          longer generate a self-signed certificate on first
-          launch</link> and will be
-          <link xlink:href="https://matrix.org/blog/2019/02/05/synapse-0-99-0/">the
-          last version to accept self-signed certificates</link>. As
-          such, it is now recommended to use a proper certificate
-          verified by a root CA (for example Let's Encrypt). The new
-          <link linkend="module-services-matrix">manual chapter on
-          Matrix</link> contains a working example of using nginx as a
-          reverse proxy in front of <literal>matrix-synapse</literal>,
-          using Let's Encrypt certificates.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>mailutils</literal> now works by default when
-          <literal>sendmail</literal> is not in a setuid wrapper. As a
-          consequence, the <literal>sendmailPath</literal> argument,
-          having lost its main use, has been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>graylog</literal> has been upgraded from version 2.*
-          to 3.*. Some setups making use of extraConfig (especially
-          those exposing Graylog via reverse proxies) need to be updated
-          as upstream removed/replaced some settings. See
-          <link xlink:href="http://docs.graylog.org/en/3.0/pages/upgrade/graylog-3.0.html#simplified-http-interface-configuration">Upgrading
-          Graylog</link> for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option <literal>users.ldap.bind.password</literal> was
-          renamed to <literal>users.ldap.bind.passwordFile</literal>,
-          and needs to be readable by the <literal>nslcd</literal> user.
-          Same applies to the new
-          <literal>users.ldap.daemon.rootpwmodpwFile</literal> option.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>nodejs-6_x</literal> is end-of-life.
-          <literal>nodejs-6_x</literal>,
-          <literal>nodejs-slim-6_x</literal> and
-          <literal>nodePackages_6_x</literal> are removed.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-19.03-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The <literal>services.matomo</literal> module gained the
-          option <literal>services.matomo.package</literal> which
-          determines the used Matomo version.
-        </para>
-        <para>
-          The Matomo module now also comes with the systemd service
-          <literal>matomo-archive-processing.service</literal> and a
-          timer that automatically triggers archive processing every
-          hour. This means that you can safely
-          <link xlink:href="https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour">
-          disable browser triggers for Matomo archiving </link> at
-          <literal>Administration &gt; System &gt; General Settings</literal>.
-        </para>
-        <para>
-          Additionally, you can enable to
-          <link xlink:href="https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs">
-          delete old visitor logs </link> at
-          <literal>Administration &gt; System &gt; Privacy</literal>,
-          but make sure that you run
-          <literal>systemctl start matomo-archive-processing.service</literal>
-          at least once without errors if you have already collected
-          data before, so that the reports get archived before the
-          source data gets deleted.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>composableDerivation</literal> along with supporting
-          library functions has been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The deprecated <literal>truecrypt</literal> package has been
-          removed and <literal>truecrypt</literal> attribute is now an
-          alias for <literal>veracrypt</literal>. VeraCrypt is
-          backward-compatible with TrueCrypt volumes. Note that
-          <literal>cryptsetup</literal> also supports loading TrueCrypt
-          volumes.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Kubernetes DNS addons, kube-dns, has been replaced with
-          CoreDNS. This change is made in accordance with Kubernetes
-          making CoreDNS the official default starting from
-          <link xlink:href="https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG-1.11.md#sig-cluster-lifecycle">Kubernetes
-          v1.11</link>. Please beware that upgrading DNS-addon on
-          existing clusters might induce minor downtime while the
-          DNS-addon terminates and re-initializes. Also note that the
-          DNS-service now runs with 2 pod replicas by default. The
-          desired number of replicas can be configured using:
-          <literal>services.kubernetes.addons.dns.replicas</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The quassel-webserver package and module was removed from
-          nixpkgs due to the lack of maintainers.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The manual gained a <link linkend="module-services-matrix">
-          new chapter on self-hosting <literal>matrix-synapse</literal>
-          and <literal>riot-web</literal> </link>, the most prevalent
-          server and client implementations for the
-          <link xlink:href="https://matrix.org/">Matrix</link> federated
-          communication network.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The astah-community package was removed from nixpkgs due to it
-          being discontinued and the downloads not being available
-          anymore.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The httpd service now saves log files with a .log file
-          extension by default for easier integration with the logrotate
-          service.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The owncloud server packages and httpd subservice module were
-          removed from nixpkgs due to the lack of maintainers.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          It is possible now to uze ZRAM devices as general purpose
-          ephemeral block devices, not only as swap. Using more than 1
-          device as ZRAM swap is no longer recommended, but is still
-          possible by setting <literal>zramSwap.swapDevices</literal>
-          explicitly.
-        </para>
-        <para>
-          ZRAM algorithm can be changed now.
-        </para>
-        <para>
-          Changes to ZRAM algorithm are applied during
-          <literal>nixos-rebuild switch</literal>, so make sure you have
-          enough swap space on disk to survive ZRAM device rebuild.
-          Alternatively, use
-          <literal>nixos-rebuild boot; reboot</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Flat volumes are now disabled by default in
-          <literal>hardware.pulseaudio</literal>. This has been done to
-          prevent applications, which are unaware of this feature,
-          setting their volumes to 100% on startup causing harm to your
-          audio hardware and potentially your ears.
-        </para>
-        <note>
-          <para>
-            With this change application specific volumes are relative
-            to the master volume which can be adjusted independently,
-            whereas before they were absolute; meaning that in effect,
-            it scaled the device-volume with the volume of the loudest
-            application.
-          </para>
-        </note>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="https://github.com/DanielAdolfsson/ndppd"><literal>ndppd</literal></link>
-          module now supports
-          <link xlink:href="options.html#opt-services.ndppd.enable">all
-          config options</link> provided by the current upstream version
-          as service options. Additionally the <literal>ndppd</literal>
-          package doesn't contain the systemd unit configuration from
-          upstream anymore, the unit is completely configured by the
-          NixOS module now.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          New installs of NixOS will default to the Redmine 4.x series
-          unless otherwise specified in
-          <literal>services.redmine.package</literal> while existing
-          installs of NixOS will default to the Redmine 3.x series.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.grafana.enable">Grafana
-          module</link> now supports declarative
-          <link xlink:href="http://docs.grafana.org/administration/provisioning/">datasource
-          and dashboard</link> provisioning.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The use of insecure ports on kubernetes has been deprecated.
-          Thus options:
-          <literal>services.kubernetes.apiserver.port</literal> and
-          <literal>services.kubernetes.controllerManager.port</literal>
-          has been renamed to <literal>.insecurePort</literal>, and
-          default of both options has changed to 0 (disabled).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Note that the default value of
-          <literal>services.kubernetes.apiserver.bindAddress</literal>
-          has changed from 127.0.0.1 to 0.0.0.0, allowing the apiserver
-          to be accessible from outside the master node itself. If the
-          apiserver insecurePort is enabled, it is strongly recommended
-          to only bind on the loopback interface. See:
-          <literal>services.kubernetes.apiserver.insecurebindAddress</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <literal>services.kubernetes.apiserver.allowPrivileged</literal>
-          and
-          <literal>services.kubernetes.kubelet.allowPrivileged</literal>
-          now defaults to false. Disallowing privileged containers on
-          the cluster.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The kubernetes module does no longer add the kubernetes
-          package to <literal>environment.systemPackages</literal>
-          implicitly.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>intel</literal> driver has been removed from the
-          default list of
-          <link xlink:href="options.html#opt-services.xserver.videoDrivers">X.org
-          video drivers</link>. The <literal>modesetting</literal>
-          driver should take over automatically, it is better maintained
-          upstream and has less problems with advanced X11 features.
-          This can lead to a change in the output names used by
-          <literal>xrandr</literal>. Some performance regressions on
-          some GPU models might happen. Some OpenCL and VA-API
-          applications might also break (Beignet seems to provide OpenCL
-          support with <literal>modesetting</literal> driver, too).
-          Kernel mode setting API does not support backlight control, so
-          <literal>xbacklight</literal> tool will not work; backlight
-          level can be controlled directly via <literal>/sys/</literal>
-          or with <literal>brightnessctl</literal>. Users who need this
-          functionality more than multi-output XRandR are advised to add
-          `intel` to `videoDrivers` and report an issue (or provide
-          additional details in an existing one)
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Openmpi has been updated to version 4.0.0, which removes some
-          deprecated MPI-1 symbols. This may break some older
-          applications that still rely on those symbols. An upgrade
-          guide can be found
-          <link xlink:href="https://www.open-mpi.org/faq/?category=mpi-removed">here</link>.
-        </para>
-        <para>
-          The nginx package now relies on OpenSSL 1.1 and supports TLS
-          1.3 by default. You can set the protocols used by the nginx
-          service using
-          <link xlink:href="options.html#opt-services.nginx.sslProtocols">services.nginx.sslProtocols</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          A new subcommand <literal>nixos-rebuild edit</literal> was
-          added.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-1909.section.xml b/nixos/doc/manual/from_md/release-notes/rl-1909.section.xml
deleted file mode 100644
index f9b99961d27..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-1909.section.xml
+++ /dev/null
@@ -1,1197 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-19.09">
-  <title>Release 19.09 (<quote>Loris</quote>, 2019/10/09)</title>
-  <section xml:id="sec-release-19.09-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      has the following highlights:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          End of support is planned for end of April 2020, handing over
-          to 20.03.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Nix has been updated to 2.3; see its
-          <link xlink:href="https://nixos.org/nix/manual/#ssec-relnotes-2.3">release
-          notes</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Core version changes:
-        </para>
-        <para>
-          systemd: 239 -&gt; 243
-        </para>
-        <para>
-          gcc: 7 -&gt; 8
-        </para>
-        <para>
-          glibc: 2.27 (unchanged)
-        </para>
-        <para>
-          linux: 4.19 LTS (unchanged)
-        </para>
-        <para>
-          openssl: 1.0 -&gt; 1.1
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Desktop version changes:
-        </para>
-        <para>
-          plasma5: 5.14 -&gt; 5.16
-        </para>
-        <para>
-          gnome3: 3.30 -&gt; 3.32
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PHP now defaults to PHP 7.3, updated from 7.2.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PHP 7.1 is no longer supported due to upstream not supporting
-          this version for the entire lifecycle of the 19.09 release.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The binfmt module is now easier to use. Additional systems can
-          be added through
-          <literal>boot.binfmt.emulatedSystems</literal>. For instance,
-          <literal>boot.binfmt.emulatedSystems = [ &quot;wasm32-wasi&quot; &quot;x86_64-windows&quot; &quot;aarch64-linux&quot; ];</literal>
-          will set up binfmt interpreters for each of those listed
-          systems.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The installer now uses a less privileged
-          <literal>nixos</literal> user whereas before we logged in as
-          root. To gain root privileges use <literal>sudo -i</literal>
-          without a password.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          We've updated to Xfce 4.14, which brings a new module
-          <literal>services.xserver.desktopManager.xfce4-14</literal>.
-          If you'd like to upgrade, please switch from the
-          <literal>services.xserver.desktopManager.xfce</literal> module
-          as it will be deprecated in a future release. They're
-          incompatibilities with the current Xfce module; it doesn't
-          support <literal>thunarPlugins</literal> and it isn't
-          recommended to use
-          <literal>services.xserver.desktopManager.xfce</literal> and
-          <literal>services.xserver.desktopManager.xfce4-14</literal>
-          simultaneously or to downgrade from Xfce 4.14 after upgrading.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The GNOME 3 desktop manager module sports an interface to
-          enable/disable core services, applications, and optional GNOME
-          packages like games.
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>services.gnome3.core-os-services.enable</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.gnome3.core-shell.enable</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.gnome3.core-utilities.enable</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.gnome3.games.enable</literal>
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          With these options we hope to give users finer grained control
-          over their systems. Prior to this change you'd either have to
-          manually disable options or use
-          <literal>environment.gnome3.excludePackages</literal> which
-          only excluded the optional applications.
-          <literal>environment.gnome3.excludePackages</literal> is now
-          unguarded, it can exclude any package installed with
-          <literal>environment.systemPackages</literal> in the GNOME 3
-          module.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Orthogonal to the previous changes to the GNOME 3 desktop
-          manager module, we've updated all default services and
-          applications to match as close as possible to a default
-          reference GNOME 3 experience.
-        </para>
-        <para>
-          <emphasis role="strong">The following changes were enacted in
-          <literal>services.gnome3.core-utilities.enable</literal></emphasis>
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>accerciser</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>dconf-editor</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>evolution</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>gnome-documents</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>gnome-nettool</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>gnome-power-manager</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>gnome-todo</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>gnome-tweaks</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>gnome-usage</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>gucharmap</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>nautilus-sendto</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>vinagre</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>cheese</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>geary</literal>
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          <emphasis role="strong">The following changes were enacted in
-          <literal>services.gnome3.core-shell.enable</literal></emphasis>
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>gnome-color-manager</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>orca</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.avahi.enable</literal>
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-19.09-new-services">
-    <title>New Services</title>
-    <para>
-      The following new services were added since the last release:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>./programs/dwm-status.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The new <literal>hardware.printers</literal> module allows to
-          declaratively configure CUPS printers via the
-          <literal>ensurePrinters</literal> and
-          <literal>ensureDefaultPrinter</literal> options.
-          <literal>ensurePrinters</literal> will never delete existing
-          printers, but will make sure that the given printers are
-          configured as declared.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          There is a new
-          <link xlink:href="options.html#opt-services.system-config-printer.enable">services.system-config-printer.enable</link>
-          and
-          <link xlink:href="options.html#opt-programs.system-config-printer.enable">programs.system-config-printer.enable</link>
-          module for the program of the same name. If you previously had
-          <literal>system-config-printer</literal> enabled through some
-          other means you should migrate to using one of these modules.
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>services.xserver.desktopManager.plasma5</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.xserver.desktopManager.gnome3</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.xserver.desktopManager.pantheon</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.xserver.desktopManager.mate</literal>
-              Note Mate uses
-              <literal>programs.system-config-printer</literal> as it
-              doesn't use it as a service, but its graphical interface
-              directly.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="options.html#opt-services.blueman.enable">services.blueman.enable</link>
-          has been added. If you previously had blueman installed via
-          <literal>environment.systemPackages</literal> please migrate
-          to using the NixOS module, as this would result in an
-          insufficiently configured blueman.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-19.09-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <para>
-      When upgrading from a previous release, please be aware of the
-      following incompatible changes:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Buildbot no longer supports Python 2, as support was dropped
-          upstream in version 2.0.0. Configurations may need to be
-          modified to make them compatible with Python 3.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PostgreSQL now uses <literal>/run/postgresql</literal> as its
-          socket directory instead of <literal>/tmp</literal>. So if you
-          run an application like eg. Nextcloud, where you need to use
-          the Unix socket path as the database host name, you need to
-          change it accordingly.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PostgreSQL 9.4 is scheduled EOL during the 19.09 life cycle
-          and has been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The options
-          <literal>services.prometheus.alertmanager.user</literal> and
-          <literal>services.prometheus.alertmanager.group</literal> have
-          been removed because the alertmanager service is now using
-          systemd's
-          <link xlink:href="http://0pointer.net/blog/dynamic-users-with-systemd.html">
-          DynamicUser mechanism</link> which obviates these options.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The NetworkManager systemd unit was renamed back from
-          network-manager.service to NetworkManager.service for better
-          compatibility with other applications expecting this name. The
-          same applies to ModemManager where modem-manager.service is
-          now called ModemManager.service again.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.nzbget.configFile</literal> and
-          <literal>services.nzbget.openFirewall</literal> options were
-          removed as they are managed internally by the nzbget. The
-          <literal>services.nzbget.dataDir</literal> option hadn't
-          actually been used by the module for some time and so was
-          removed as cleanup.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.mysql.pidDir</literal> option was
-          removed, as it was only used by the wordpress apache-httpd
-          service to wait for mysql to have started up. This can be
-          accomplished by either describing a dependency on
-          mysql.service (preferred) or waiting for the (hardcoded)
-          <literal>/run/mysqld/mysql.sock</literal> file to appear.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.emby.enable</literal> module has been
-          removed, see <literal>services.jellyfin.enable</literal>
-          instead for a free software fork of Emby. See the Jellyfin
-          documentation:
-          <link xlink:href="https://jellyfin.readthedocs.io/en/latest/administrator-docs/migrate-from-emby/">
-          Migrating from Emby to Jellyfin </link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          IPv6 Privacy Extensions are now enabled by default for
-          undeclared interfaces. The previous behaviour was quite
-          misleading — even though the default value for
-          <literal>networking.interfaces.*.preferTempAddress</literal>
-          was <literal>true</literal>, undeclared interfaces would not
-          prefer temporary addresses. Now, interfaces not mentioned in
-          the config will prefer temporary addresses. EUI64 addresses
-          can still be set as preferred by explicitly setting the option
-          to <literal>false</literal> for the interface in question.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Since Bittorrent Sync was superseded by Resilio Sync in 2016,
-          the <literal>bittorrentSync</literal>,
-          <literal>bittorrentSync14</literal>, and
-          <literal>bittorrentSync16</literal> packages have been removed
-          in favor of <literal>resilio-sync</literal>.
-        </para>
-        <para>
-          The corresponding module, <literal>services.btsync</literal>
-          has been replaced by the <literal>services.resilio</literal>
-          module.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The httpd service no longer attempts to start the postgresql
-          service. If you have come to depend on this behaviour then you
-          can preserve the behavior with the following configuration:
-          <literal>systemd.services.httpd.after = [ &quot;postgresql.service&quot; ];</literal>
-        </para>
-        <para>
-          The option <literal>services.httpd.extraSubservices</literal>
-          has been marked as deprecated. You may still use this feature,
-          but it will be removed in a future release of NixOS. You are
-          encouraged to convert any httpd subservices you may have
-          written to a full NixOS module.
-        </para>
-        <para>
-          Most of the httpd subservices packaged with NixOS have been
-          replaced with full NixOS modules including LimeSurvey,
-          WordPress, and Zabbix. These modules can be enabled using the
-          <literal>services.limesurvey.enable</literal>,
-          <literal>services.mediawiki.enable</literal>,
-          <literal>services.wordpress.enable</literal>, and
-          <literal>services.zabbixWeb.enable</literal> options.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <literal>systemd.network.networks.&lt;name&gt;.routes.*.routeConfig.GatewayOnlink</literal>
-          was renamed to
-          <literal>systemd.network.networks.&lt;name&gt;.routes.*.routeConfig.GatewayOnLink</literal>
-          (capital <literal>L</literal>). This follows
-          <link xlink:href="https://github.com/systemd/systemd/commit/9cb8c5593443d24c19e40bfd4fc06d672f8c554c">
-          upstreams renaming </link> of the setting.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          As of this release the NixOps feature
-          <literal>autoLuks</literal> is deprecated. It no longer works
-          with our systemd version without manual intervention.
-        </para>
-        <para>
-          Whenever the usage of the module is detected the evaluation
-          will fail with a message explaining why and how to deal with
-          the situation.
-        </para>
-        <para>
-          A new knob named
-          <literal>nixops.enableDeprecatedAutoLuks</literal> has been
-          introduced to disable the eval failure and to acknowledge the
-          notice was received and read. If you plan on using the feature
-          please note that it might break with subsequent updates.
-        </para>
-        <para>
-          Make sure you set the <literal>_netdev</literal> option for
-          each of the file systems referring to block devices provided
-          by the autoLuks module. Not doing this might render the system
-          in a state where it doesn't boot anymore.
-        </para>
-        <para>
-          If you are actively using the <literal>autoLuks</literal>
-          module please let us know in
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/62211">issue
-          #62211</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The setopt declarations will be evaluated at the end of
-          <literal>/etc/zshrc</literal>, so any code in
-          <link xlink:href="options.html#opt-programs.zsh.interactiveShellInit">programs.zsh.interactiveShellInit</link>,
-          <link xlink:href="options.html#opt-programs.zsh.loginShellInit">programs.zsh.loginShellInit</link>
-          and
-          <link xlink:href="options.html#opt-programs.zsh.promptInit">programs.zsh.promptInit</link>
-          may break if it relies on those options being set.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>prometheus-nginx-exporter</literal> package now
-          uses the official exporter provided by NGINX Inc. Its metrics
-          are differently structured and are incompatible to the old
-          ones. For information about the metrics, have a look at the
-          <link xlink:href="https://github.com/nginxinc/nginx-prometheus-exporter">official
-          repo</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>shibboleth-sp</literal> package has been updated
-          to version 3. It is largely backward compatible, for further
-          information refer to the
-          <link xlink:href="https://wiki.shibboleth.net/confluence/display/SP3/ReleaseNotes">release
-          notes</link> and
-          <link xlink:href="https://wiki.shibboleth.net/confluence/display/SP3/UpgradingFromV2">upgrade
-          guide</link>.
-        </para>
-        <para>
-          Nodejs 8 is scheduled EOL under the lifetime of 19.09 and has
-          been dropped.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          By default, prometheus exporters are now run with
-          <literal>DynamicUser</literal> enabled. Exporters that need a
-          real user, now run under a separate user and group which
-          follow the pattern
-          <literal>&lt;exporter-name&gt;-exporter</literal>, instead of
-          the previous default <literal>nobody</literal> and
-          <literal>nogroup</literal>. Only some exporters are affected
-          by the latter, namely the exporters
-          <literal>dovecot</literal>, <literal>node</literal>,
-          <literal>postfix</literal> and <literal>varnish</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>ibus-qt</literal> package is not installed by
-          default anymore when
-          <link xlink:href="options.html#opt-i18n.inputMethod.enabled">i18n.inputMethod.enabled</link>
-          is set to <literal>ibus</literal>. If IBus support in Qt 4.x
-          applications is required, add the <literal>ibus-qt</literal>
-          package to your
-          <link xlink:href="options.html#opt-environment.systemPackages">environment.systemPackages</link>
-          manually.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The CUPS Printing service now uses socket-based activation by
-          default, only starting when needed. The previous behavior can
-          be restored by setting
-          <literal>services.cups.startWhenNeeded</literal> to
-          <literal>false</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.systemhealth</literal> module has been
-          removed from nixpkgs due to lack of maintainer.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.mantisbt</literal> module has been
-          removed from nixpkgs due to lack of maintainer.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Squid 3 has been removed and the <literal>squid</literal>
-          derivation now refers to Squid 4.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.pdns-recursor.extraConfig</literal>
-          option has been replaced by
-          <literal>services.pdns-recursor.settings</literal>. The new
-          option allows setting extra configuration while being better
-          type-checked and mergeable.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          No service depends on <literal>keys.target</literal> anymore
-          which is a systemd target that indicates if all
-          <link xlink:href="https://nixos.org/nixops/manual/#idm140737322342384">NixOps
-          keys</link> were successfully uploaded. Instead,
-          <literal>&lt;key-name&gt;-key.service</literal> should be used
-          to define a dependency of a key in a service. The full issue
-          behind the <literal>keys.target</literal> dependency is
-          described at
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/67265">NixOS/nixpkgs#67265</link>.
-        </para>
-        <para>
-          The following services are affected by this:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.dovecot2.enable"><literal>services.dovecot2</literal></link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.nsd.enable"><literal>services.nsd</literal></link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.softether.enable"><literal>services.softether</literal></link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.strongswan.enable"><literal>services.strongswan</literal></link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.strongswan-swanctl.enable"><literal>services.strongswan-swanctl</literal></link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.httpd.enable"><literal>services.httpd</literal></link>
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>security.acme.directory</literal> option has been
-          replaced by a read-only
-          <literal>security.acme.certs.&lt;cert&gt;.directory</literal>
-          option for each certificate you define. This will be a
-          subdirectory of <literal>/var/lib/acme</literal>. You can use
-          this read-only option to figure out where the certificates are
-          stored for a specific certificate. For example, the
-          <literal>services.nginx.virtualhosts.&lt;name&gt;.enableACME</literal>
-          option will use this directory option to find the certs for
-          the virtual host.
-        </para>
-        <para>
-          <literal>security.acme.preDelay</literal> and
-          <literal>security.acme.activationDelay</literal> options have
-          been removed. To execute a service before certificates are
-          provisioned or renewed add a
-          <literal>RequiredBy=acme-${cert}.service</literal> to any
-          service.
-        </para>
-        <para>
-          Furthermore, the acme module will not automatically add a
-          dependency on <literal>lighttpd.service</literal> anymore. If
-          you are using certficates provided by letsencrypt for
-          lighttpd, then you should depend on the certificate service
-          <literal>acme-${cert}.service&gt;</literal> manually.
-        </para>
-        <para>
-          For nginx, the dependencies are still automatically managed
-          when
-          <literal>services.nginx.virtualhosts.&lt;name&gt;.enableACME</literal>
-          is enabled just like before. What changed is that nginx now
-          directly depends on the specific certificates that it needs,
-          instead of depending on the catch-all
-          <literal>acme-certificates.target</literal>. This target unit
-          was also removed from the codebase. This will mean nginx will
-          no longer depend on certificates it isn't explicitly managing
-          and fixes a bug with certificate renewal ordering racing with
-          nginx restarting which could lead to nginx getting in a broken
-          state as described at
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/60180">NixOS/nixpkgs#60180</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The old deprecated <literal>emacs</literal> package sets have
-          been dropped. What used to be called
-          <literal>emacsPackagesNg</literal> is now simply called
-          <literal>emacsPackages</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.xserver.desktopManager.xterm</literal> is
-          now disabled by default if <literal>stateVersion</literal> is
-          19.09 or higher. Previously the xterm desktopManager was
-          enabled when xserver was enabled, but it isn't useful for all
-          people so it didn't make sense to have any desktopManager
-          enabled default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The WeeChat plugin
-          <literal>pkgs.weechatScripts.weechat-xmpp</literal> has been
-          removed as it doesn't receive any updates from upstream and
-          depends on outdated Python2-based modules.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Old unsupported versions (<literal>logstash5</literal>,
-          <literal>kibana5</literal>, <literal>filebeat5</literal>,
-          <literal>heartbeat5</literal>, <literal>metricbeat5</literal>,
-          <literal>packetbeat5</literal>) of the ELK-stack and Elastic
-          beats have been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          For NixOS 19.03, both Prometheus 1 and 2 were available to
-          allow for a seamless transition from version 1 to 2 with
-          existing setups. Because Prometheus 1 is no longer developed,
-          it was removed. Prometheus 2 is now configured with
-          <literal>services.prometheus</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Citrix Receiver (<literal>citrix_receiver</literal>) has been
-          dropped in favor of Citrix Workspace
-          (<literal>citrix_workspace</literal>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.gitlab</literal> module has had its
-          literal secret options
-          (<literal>services.gitlab.smtp.password</literal>,
-          <literal>services.gitlab.databasePassword</literal>,
-          <literal>services.gitlab.initialRootPassword</literal>,
-          <literal>services.gitlab.secrets.secret</literal>,
-          <literal>services.gitlab.secrets.db</literal>,
-          <literal>services.gitlab.secrets.otp</literal> and
-          <literal>services.gitlab.secrets.jws</literal>) replaced by
-          file-based versions
-          (<literal>services.gitlab.smtp.passwordFile</literal>,
-          <literal>services.gitlab.databasePasswordFile</literal>,
-          <literal>services.gitlab.initialRootPasswordFile</literal>,
-          <literal>services.gitlab.secrets.secretFile</literal>,
-          <literal>services.gitlab.secrets.dbFile</literal>,
-          <literal>services.gitlab.secrets.otpFile</literal> and
-          <literal>services.gitlab.secrets.jwsFile</literal>). This was
-          done so that secrets aren't stored in the world-readable nix
-          store, but means that for each option you'll have to create a
-          file with the same exact string, add &quot;File&quot; to the
-          end of the option name, and change the definition to a string
-          pointing to the corresponding file; e.g.
-          <literal>services.gitlab.databasePassword = &quot;supersecurepassword&quot;</literal>
-          becomes
-          <literal>services.gitlab.databasePasswordFile = &quot;/path/to/secret_file&quot;</literal>
-          where the file <literal>secret_file</literal> contains the
-          string <literal>supersecurepassword</literal>.
-        </para>
-        <para>
-          The state path (<literal>services.gitlab.statePath</literal>)
-          now has the following restriction: no parent directory can be
-          owned by any other user than <literal>root</literal> or the
-          user specified in <literal>services.gitlab.user</literal>;
-          i.e. if <literal>services.gitlab.statePath</literal> is set to
-          <literal>/var/lib/gitlab/state</literal>,
-          <literal>gitlab</literal> and all parent directories must be
-          owned by either <literal>root</literal> or the user specified
-          in <literal>services.gitlab.user</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>networking.useDHCP</literal> option is
-          unsupported in combination with
-          <literal>networking.useNetworkd</literal> in anticipation of
-          defaulting to it. It has to be set to <literal>false</literal>
-          and enabled per interface with
-          <literal>networking.interfaces.&lt;name&gt;.useDHCP = true;</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Twitter client <literal>corebird</literal> has been
-          dropped as
-          <link xlink:href="https://www.patreon.com/posts/corebirds-future-18921328">it
-          is discontinued and does not work against the new Twitter
-          API</link>. Please use the fork <literal>cawbird</literal>
-          instead which has been adapted to the API changes and is still
-          maintained.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>nodejs-11_x</literal> package has been removed as
-          it's EOLed by upstream.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Because of the systemd upgrade, systemd-timesyncd will no
-          longer work if <literal>system.stateVersion</literal> is not
-          set correctly. When upgrading from NixOS 19.03, please make
-          sure that <literal>system.stateVersion</literal> is set to
-          <literal>&quot;19.03&quot;</literal>, or lower if the
-          installation dates back to an earlier version of NixOS.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Due to the short lifetime of non-LTS kernel releases package
-          attributes like <literal>linux_5_1</literal>,
-          <literal>linux_5_2</literal> and <literal>linux_5_3</literal>
-          have been removed to discourage dependence on specific non-LTS
-          kernel versions in stable NixOS releases. Going forward,
-          versioned attributes like <literal>linux_4_9</literal> will
-          exist for LTS versions only. Please use
-          <literal>linux_latest</literal> or
-          <literal>linux_testing</literal> if you depend on non-LTS
-          releases. Keep in mind that <literal>linux_latest</literal>
-          and <literal>linux_testing</literal> will change versions
-          under the hood during the lifetime of a stable release and
-          might include breaking changes.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Because of the systemd upgrade, some network interfaces might
-          change their name. For details see
-          <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.net-naming-scheme.html#History">
-          upstream docs</link> or
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/71086">
-          our ticket</link>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-19.09-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The <literal>documentation</literal> module gained an option
-          named <literal>documentation.nixos.includeAllModules</literal>
-          which makes the generated configuration.nix 5 manual page
-          include all options from all NixOS modules included in a given
-          <literal>configuration.nix</literal> configuration file.
-          Currently, it is set to <literal>false</literal> by default as
-          enabling it frequently prevents evaluation. But the plan is to
-          eventually have it set to <literal>true</literal> by default.
-          Please set it to <literal>true</literal> now in your
-          <literal>configuration.nix</literal> and fix all the bugs it
-          uncovers.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>vlc</literal> package gained support for
-          Chromecast streaming, enabled by default. TCP port 8010 must
-          be open for it to work, so something like
-          <literal>networking.firewall.allowedTCPPorts = [ 8010 ];</literal>
-          may be required in your configuration. Also consider enabling
-          <link xlink:href="https://nixos.wiki/wiki/Accelerated_Video_Playback">
-          Accelerated Video Playback</link> for better transcoding
-          performance.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The following changes apply if the
-          <literal>stateVersion</literal> is changed to 19.09 or higher.
-          For <literal>stateVersion = &quot;19.03&quot;</literal> or
-          lower the old behavior is preserved.
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              <literal>solr.package</literal> defaults to
-              <literal>pkgs.solr_8</literal>.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>hunspellDicts.fr-any</literal> dictionary now
-          ships with <literal>fr_FR.{aff,dic}</literal> which is linked
-          to <literal>fr-toutesvariantes.{aff,dic}</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>mysql</literal> service now runs as
-          <literal>mysql</literal> user. Previously, systemd did execute
-          it as root, and mysql dropped privileges itself. This includes
-          <literal>ExecStartPre=</literal> and
-          <literal>ExecStartPost=</literal> phases. To accomplish that,
-          runtime and data directory setup was delegated to
-          RuntimeDirectory and tmpfiles.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          With the upgrade to systemd version 242 the
-          <literal>systemd-timesyncd</literal> service is no longer
-          using <literal>DynamicUser=yes</literal>. In order for the
-          upgrade to work we rely on an activation script to move the
-          state from the old to the new directory. The older directory
-          (prior <literal>19.09</literal>) was
-          <literal>/var/lib/private/systemd/timesync</literal>.
-        </para>
-        <para>
-          As long as the <literal>system.config.stateVersion</literal>
-          is below <literal>19.09</literal> the state folder will
-          migrated to its proper location
-          (<literal>/var/lib/systemd/timesync</literal>), if required.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The package <literal>avahi</literal> is now built to look up
-          service definitions from
-          <literal>/etc/avahi/services</literal> instead of its output
-          directory in the nix store. Accordingly the module
-          <literal>avahi</literal> now supports custom service
-          definitions via
-          <literal>services.avahi.extraServiceFiles</literal>, which are
-          then placed in the aforementioned directory. See
-          avahi.service5 for more information on custom service
-          definitions.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Since version 0.1.19, <literal>cargo-vendor</literal> honors
-          package includes that are specified in the
-          <literal>Cargo.toml</literal> file of Rust crates.
-          <literal>rustPlatform.buildRustPackage</literal> uses
-          <literal>cargo-vendor</literal> to collect and build dependent
-          crates. Since this change in <literal>cargo-vendor</literal>
-          changes the set of vendored files for most Rust packages, the
-          hash that use used to verify the dependencies,
-          <literal>cargoSha256</literal>, also changes.
-        </para>
-        <para>
-          The <literal>cargoSha256</literal> hashes of all in-tree
-          derivations that use <literal>buildRustPackage</literal> have
-          been updated to reflect this change. However, third-party
-          derivations that use <literal>buildRustPackage</literal> may
-          have to be updated as well.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>consul</literal> package was upgraded past
-          version <literal>1.5</literal>, so its deprecated legacy UI is
-          no longer available.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default resample-method for PulseAudio has been changed
-          from the upstream default <literal>speex-float-1</literal> to
-          <literal>speex-float-5</literal>. Be aware that low-powered
-          ARM-based and MIPS-based boards will struggle with this so
-          you'll need to set
-          <literal>hardware.pulseaudio.daemon.config.resample-method</literal>
-          back to <literal>speex-float-1</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>phabricator</literal> package and associated
-          <literal>httpd.extraSubservice</literal>, as well as the
-          <literal>phd</literal> service have been removed from nixpkgs
-          due to lack of maintainer.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>mercurial</literal>
-          <literal>httpd.extraSubservice</literal> has been removed from
-          nixpkgs due to lack of maintainer.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>trac</literal>
-          <literal>httpd.extraSubservice</literal> has been removed from
-          nixpkgs because it was unmaintained.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>foswiki</literal> package and associated
-          <literal>httpd.extraSubservice</literal> have been removed
-          from nixpkgs due to lack of maintainer.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>tomcat-connector</literal>
-          <literal>httpd.extraSubservice</literal> has been removed from
-          nixpkgs.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          It's now possible to change configuration in
-          <link xlink:href="options.html#opt-services.nextcloud.enable">services.nextcloud</link>
-          after the initial deploy since all config parameters are
-          persisted in an additional config file generated by the
-          module. Previously core configuration like database parameters
-          were set using their imperative installer after creating
-          <literal>/var/lib/nextcloud</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          There exists now <literal>lib.forEach</literal>, which is like
-          <literal>map</literal>, but with arguments flipped. When
-          mapping function body spans many lines (or has nested
-          <literal>map</literal>s), it is often hard to follow which
-          list is modified.
-        </para>
-        <para>
-          Previous solution to this problem was either to use
-          <literal>lib.flip map</literal> idiom or extract that
-          anonymous mapping function to a named one. Both can still be
-          used but <literal>lib.forEach</literal> is preferred over
-          <literal>lib.flip map</literal>.
-        </para>
-        <para>
-          The <literal>/etc/sysctl.d/nixos.conf</literal> file
-          containing all the options set via
-          <link xlink:href="options.html#opt-boot.kernel.sysctl">boot.kernel.sysctl</link>
-          was moved to <literal>/etc/sysctl.d/60-nixos.conf</literal>,
-          as sysctl.d5 recommends prefixing all filenames in
-          <literal>/etc/sysctl.d</literal> with a two-digit number and a
-          dash to simplify the ordering of the files.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          We now install the sysctl snippets shipped with systemd.
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              Loose reverse path filtering
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Source route filtering
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>fq_codel</literal> as a packet scheduler (this
-              helps to fight bufferbloat)
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          This also configures the kernel to pass core dumps to
-          <literal>systemd-coredump</literal>, and restricts the SysRq
-          key combinations to the sync command only. These sysctl
-          snippets can be found in
-          <literal>/etc/sysctl.d/50-*.conf</literal>, and overridden via
-          <link xlink:href="options.html#opt-boot.kernel.sysctl">boot.kernel.sysctl</link>
-          (which will place the parameters in
-          <literal>/etc/sysctl.d/60-nixos.conf</literal>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Core dumps are now processed by
-          <literal>systemd-coredump</literal> by default.
-          <literal>systemd-coredump</literal> behaviour can still be
-          modified via <literal>systemd.coredump.extraConfig</literal>.
-          To stick to the old behaviour (having the kernel dump to a
-          file called <literal>core</literal> in the working directory),
-          without piping it through <literal>systemd-coredump</literal>,
-          set <literal>systemd.coredump.enable</literal> to
-          <literal>false</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>systemd.packages</literal> option now also supports
-          generators and shutdown scripts. Old
-          <literal>systemd.generator-packages</literal> option has been
-          removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>rmilter</literal> package was removed with
-          associated module and options due deprecation by upstream
-          developer. Use <literal>rspamd</literal> in proxy mode
-          instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          systemd cgroup accounting via the
-          <link xlink:href="options.html#opt-systemd.enableCgroupAccounting">systemd.enableCgroupAccounting</link>
-          option is now enabled by default. It now also enables the more
-          recent Block IO and IP accounting features.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          We no longer enable custom font rendering settings with
-          <literal>fonts.fontconfig.penultimate.enable</literal> by
-          default. The defaults from fontconfig are sufficient.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>crashplan</literal> package and the
-          <literal>crashplan</literal> service have been removed from
-          nixpkgs due to crashplan shutting down the service, while the
-          <literal>crashplansb</literal> package and
-          <literal>crashplan-small-business</literal> service have been
-          removed from nixpkgs due to lack of maintainer.
-        </para>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.redis.enable">redis
-          module</link> was hardcoded to use the
-          <literal>redis</literal> user, <literal>/run/redis</literal>
-          as runtime directory and <literal>/var/lib/redis</literal> as
-          state directory. Note that the NixOS module for Redis now
-          disables kernel support for Transparent Huge Pages (THP),
-          because this features causes major performance problems for
-          Redis, e.g. (https://redis.io/topics/latency).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Using <literal>fonts.enableDefaultFonts</literal> adds a
-          default emoji font <literal>noto-fonts-emoji</literal>.
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>services.xserver.enable</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>programs.sway.enable</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>programs.way-cooler.enable</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.xrdp.enable</literal>
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>altcoins</literal> categorization of packages has
-          been removed. You now access these packages at the top level,
-          ie. <literal>nix-shell -p dogecoin</literal> instead of
-          <literal>nix-shell -p altcoins.dogecoin</literal>, etc.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Ceph has been upgraded to v14.2.1. See the
-          <link xlink:href="https://ceph.com/releases/v14-2-0-nautilus-released/">release
-          notes</link> for details. The mgr dashboard as well as osds
-          backed by loop-devices is no longer explicitly supported by
-          the package and module. Note: There's been some issues with
-          python-cherrypy, which is used by the dashboard and prometheus
-          mgr modules (and possibly others), hence
-          0000-dont-check-cherrypy-version.patch.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>pkgs.weechat</literal> is now compiled against
-          <literal>pkgs.python3</literal>. Weechat also recommends
-          <link xlink:href="https://weechat.org/scripts/python3/">to use
-          Python3 in their docs.</link>
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2003.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2003.section.xml
deleted file mode 100644
index 53e6e1329a9..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-2003.section.xml
+++ /dev/null
@@ -1,1497 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-20.03">
-  <title>Release 20.03 (<quote>Markhor</quote>, 2020.04/20)</title>
-  <section xml:id="sec-release-20.03-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      has the following highlights:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Support is planned until the end of October 2020, handing over
-          to 20.09.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Core version changes:
-        </para>
-        <para>
-          gcc: 8.3.0 -&gt; 9.2.0
-        </para>
-        <para>
-          glibc: 2.27 -&gt; 2.30
-        </para>
-        <para>
-          linux: 4.19 -&gt; 5.4
-        </para>
-        <para>
-          mesa: 19.1.5 -&gt; 19.3.3
-        </para>
-        <para>
-          openssl: 1.0.2u -&gt; 1.1.1d
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Desktop version changes:
-        </para>
-        <para>
-          plasma5: 5.16.5 -&gt; 5.17.5
-        </para>
-        <para>
-          kdeApplications: 19.08.2 -&gt; 19.12.3
-        </para>
-        <para>
-          gnome3: 3.32 -&gt; 3.34
-        </para>
-        <para>
-          pantheon: 5.0 -&gt; 5.1.3
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Linux kernel is updated to branch 5.4 by default (from 4.19).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Grub is updated to 2.04, adding support for booting from F2FS
-          filesystems and Btrfs volumes using zstd compression. Note
-          that some users have been unable to boot after upgrading to
-          2.04 - for more information, please see
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/61718#issuecomment-617618503">this
-          discussion</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Postgresql for NixOS service now defaults to v11.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The graphical installer image starts the graphical session
-          automatically. Before you'd be greeted by a tty and asked to
-          enter <literal>systemctl start display-manager</literal>. It
-          is now possible to disable the display-manager from running by
-          selecting the <literal>Disable display-manager</literal> quirk
-          in the boot menu.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          GNOME 3 has been upgraded to 3.34. Please take a look at their
-          <link xlink:href="https://help.gnome.org/misc/release-notes/3.34">Release
-          Notes</link> for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          If you enable the Pantheon Desktop Manager via
-          <link xlink:href="options.html#opt-services.xserver.desktopManager.pantheon.enable">services.xserver.desktopManager.pantheon.enable</link>,
-          we now default to also use
-          <link xlink:href="https://blog.elementary.io/say-hello-to-the-new-greeter/">
-          Pantheon's newly designed greeter </link>. Contrary to NixOS's
-          usual update policy, Pantheon will receive updates during the
-          cycle of NixOS 20.03 when backwards compatible.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          By default zfs pools will now be trimmed on a weekly basis.
-          Trimming is only done on supported devices (i.e. NVME or SSDs)
-          and should improve throughput and lifetime of these devices.
-          It is controlled by the
-          <literal>services.zfs.trim.enable</literal> varname. The zfs
-          scrub service
-          (<literal>services.zfs.autoScrub.enable</literal>) and the zfs
-          autosnapshot service
-          (<literal>services.zfs.autoSnapshot.enable</literal>) are now
-          only enabled if zfs is set in
-          <literal>config.boot.initrd.supportedFilesystems</literal> or
-          <literal>config.boot.supportedFilesystems</literal>. These
-          lists will automatically contain zfs as soon as any zfs
-          mountpoint is configured in <literal>fileSystems</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>nixos-option</literal> has been rewritten in C++,
-          speeding it up, improving correctness, and adding a
-          <literal>-r</literal> option which prints all options and
-          their values recursively.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.xserver.desktopManager.default</literal> and
-          <literal>services.xserver.windowManager.default</literal>
-          options were replaced by a single
-          <link xlink:href="options.html#opt-services.xserver.displayManager.defaultSession">services.xserver.displayManager.defaultSession</link>
-          option to improve support for upstream session files. If you
-          used something like:
-        </para>
-        <programlisting language="bash">
-{
-  services.xserver.desktopManager.default = &quot;xfce&quot;;
-  services.xserver.windowManager.default = &quot;icewm&quot;;
-}
-</programlisting>
-        <para>
-          you should change it to:
-        </para>
-        <programlisting language="bash">
-{
-  services.xserver.displayManager.defaultSession = &quot;xfce+icewm&quot;;
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The testing driver implementation in NixOS is now in Python
-          <literal>make-test-python.nix</literal>. This was done by
-          Jacek Galowicz
-          (<link xlink:href="https://github.com/tfc">@tfc</link>), and
-          with the collaboration of Julian Stecklina
-          (<link xlink:href="https://github.com/blitz">@blitz</link>)
-          and Jana Traue
-          (<link xlink:href="https://github.com/jtraue">@jtraue</link>).
-          All documentation has been updated to use this testing driver,
-          and a vast majority of the 286 tests in NixOS were ported to
-          python driver. In 20.09 the Perl driver implementation,
-          <literal>make-test.nix</literal>, is slated for removal. This
-          should give users of the NixOS integration framework a
-          transitory period to rewrite their tests to use the Python
-          implementation. Users of the Perl driver will see this warning
-          everytime they use it:
-        </para>
-        <programlisting>
-$ warning: Perl VM tests are deprecated and will be removed for 20.09.
-Please update your tests to use the python test driver.
-See https://github.com/NixOS/nixpkgs/pull/71684 for details.
-</programlisting>
-        <para>
-          API compatibility is planned to be kept for at least the next
-          release with the perl driver.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-20.03-new-services">
-    <title>New Services</title>
-    <para>
-      The following new services were added since the last release:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The kubernetes kube-proxy now supports a new hostname
-          configuration
-          <literal>services.kubernetes.proxy.hostname</literal> which
-          has to be set if the hostname of the node should be non
-          default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          UPower's configuration is now managed by NixOS and can be
-          customized via <literal>services.upower</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          To use Geary you should enable
-          <link xlink:href="options.html#opt-programs.geary.enable">programs.geary.enable</link>
-          instead of just adding it to
-          <link xlink:href="options.html#opt-environment.systemPackages">environment.systemPackages</link>.
-          It was created so Geary could function properly outside of
-          GNOME.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./config/console.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./hardware/brillo.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./hardware/tuxedo-keyboard.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/bandwhich.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/bash-my-aws.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/liboping.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./programs/traceroute.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/backup/sanoid.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/backup/syncoid.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/backup/zfs-replication.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/continuous-integration/buildkite-agents.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/databases/victoriametrics.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/desktops/gnome3/gnome-initial-setup.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/desktops/neard.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/games/openarena.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/hardware/fancontrol.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/mail/sympa.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/freeswitch.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/misc/mame.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/monitoring/do-agent.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/monitoring/prometheus/xmpp-alerts.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/network-filesystems/orangefs/server.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/network-filesystems/orangefs/client.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/3proxy.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/corerad.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/go-shadowsocks2.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/ntp/openntpd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/shorewall.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/shorewall6.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/spacecookie.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/trickster.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/v2ray.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/xandikos.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/networking/yggdrasil.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/dokuwiki.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/gotify-server.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/grocy.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/ihatemoney</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/moinmoin.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/trac.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/trilium.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-apps/shiori.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/web-servers/ttyd.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/x11/picom.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/x11/hardware/digimend.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./services/x11/imwheel.nix</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>./virtualisation/cri-o.nix</literal>
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-20.03-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <para>
-      When upgrading from a previous release, please be aware of the
-      following incompatible changes:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The dhcpcd package
-          <link xlink:href="https://roy.marples.name/archives/dhcpcd-discuss/0002621.html">
-          does not request IPv4 addresses for tap and bridge interfaces
-          anymore by default</link>. In order to still get an address on
-          a bridge interface, one has to disable
-          <literal>networking.useDHCP</literal> and explicitly enable
-          <literal>networking.interfaces.&lt;name&gt;.useDHCP</literal>
-          on every interface, that should get an address via DHCP. This
-          way, dhcpcd is configured in an explicit way about which
-          interface to run on.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          GnuPG is now built without support for a graphical passphrase
-          entry by default. Please enable the
-          <literal>gpg-agent</literal> user service via the NixOS option
-          <literal>programs.gnupg.agent.enable</literal>. Note that
-          upstream recommends using <literal>gpg-agent</literal> and
-          will spawn a <literal>gpg-agent</literal> on the first
-          invocation of GnuPG anyway.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>dynamicHosts</literal> option has been removed
-          from the
-          <link xlink:href="options.html#opt-networking.networkmanager.enable">NetworkManager</link>
-          module. Allowing (multiple) regular users to override host
-          entries affecting the whole system opens up a huge attack
-          vector. There seem to be very rare cases where this might be
-          useful. Consider setting system-wide host entries using
-          <link xlink:href="options.html#opt-networking.hosts">networking.hosts</link>,
-          provide them via the DNS server in your network, or use
-          <link xlink:href="options.html#opt-environment.etc">environment.etc</link>
-          to add a file into
-          <literal>/etc/NetworkManager/dnsmasq.d</literal> reconfiguring
-          <literal>hostsdir</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>99-main.network</literal> file was removed.
-          Matching all network interfaces caused many breakages, see
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/18962">#18962</link>
-          and
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/71106">#71106</link>.
-        </para>
-        <para>
-          We already don't support the global
-          <link xlink:href="options.html#opt-networking.useDHCP">networking.useDHCP</link>,
-          <link xlink:href="options.html#opt-networking.defaultGateway">networking.defaultGateway</link>
-          and
-          <link xlink:href="options.html#opt-networking.defaultGateway6">networking.defaultGateway6</link>
-          options if
-          <link xlink:href="options.html#opt-networking.useNetworkd">networking.useNetworkd</link>
-          is enabled, but direct users to configure the per-device
-          <link xlink:href="options.html#opt-networking.interfaces">networking.interfaces.&lt;name&gt;….</link>
-          options.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The stdenv now runs all bash with <literal>set -u</literal>,
-          to catch the use of undefined variables. Before, it itself
-          used <literal>set -u</literal> but was careful to unset it so
-          other packages' code ran as before. Now, all bash code is held
-          to the same high standard, and the rather complex stateful
-          manipulation of the options can be discarded.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The SLIM Display Manager has been removed, as it has been
-          unmaintained since 2013. Consider migrating to a different
-          display manager such as LightDM (current default in NixOS),
-          SDDM, GDM, or using the startx module which uses Xinitrc.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Way Cooler wayland compositor has been removed, as the
-          project has been officially canceled. There are no more
-          <literal>way-cooler</literal> attribute and
-          <literal>programs.way-cooler</literal> options.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The BEAM package set has been deleted. You will only find
-          there the different interpreters. You should now use the
-          different build tools coming with the languages with sandbox
-          mode disabled.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          There is now only one Xfce package-set and module. This means
-          that attributes <literal>xfce4-14</literal> and
-          <literal>xfceUnstable</literal> all now point to the latest
-          Xfce 4.14 packages. And in the future NixOS releases will be
-          the latest released version of Xfce available at the time of
-          the release's development (if viable).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.phpfpm.pools">phpfpm</link>
-          module now sets <literal>PrivateTmp=true</literal> in its
-          systemd units for better process isolation. If you rely on
-          <literal>/tmp</literal> being shared with other services,
-          explicitly override this by setting
-          <literal>serviceConfig.PrivateTmp</literal> to
-          <literal>false</literal> for each phpfpm unit.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          KDE’s old multimedia framework Phonon no longer supports Qt 4.
-          For that reason, Plasma desktop also does not have
-          <literal>enableQt4Support</literal> option any more.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The BeeGFS module has been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The osquery module has been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Going forward, <literal>~/bin</literal> in the users home
-          directory will no longer be in <literal>PATH</literal> by
-          default. If you depend on this you should set the option
-          <literal>environment.homeBinInPath</literal> to
-          <literal>true</literal>. The aforementioned option was added
-          this release.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>buildRustCrate</literal> infrastructure now
-          produces <literal>lib</literal> outputs in addition to the
-          <literal>out</literal> output. This has led to drastically
-          reduced closure sizes for some rust crates since development
-          dependencies are now in the <literal>lib</literal> output.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Pango was upgraded to 1.44, which no longer uses freetype for
-          font loading. This means that type1 and bitmap fonts are no
-          longer supported in applications relying on Pango for font
-          rendering (notably, GTK application). See
-          <link xlink:href="https://gitlab.gnome.org/GNOME/pango/issues/386">
-          upstream issue</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>roundcube</literal> module has been hardened.
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              The password of the database is not written world readable
-              in the store any more. If <literal>database.host</literal>
-              is set to <literal>localhost</literal>, then a unix user
-              of the same name as the database will be created and
-              PostreSQL peer authentication will be used, removing the
-              need for a password. Otherwise, a password is still needed
-              and can be provided with the new option
-              <literal>database.passwordFile</literal>, which should be
-              set to the path of a file containing the password and
-              readable by the user <literal>nginx</literal> only. The
-              <literal>database.password</literal> option is insecure
-              and deprecated. Usage of this option will print a warning.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              A random <literal>des_key</literal> is set by default in
-              the configuration of roundcube, instead of using the
-              hardcoded and insecure default. To ensure a clean
-              migration, all users will be logged out when you upgrade
-              to this release.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The packages <literal>openobex</literal> and
-          <literal>obexftp</literal> are no longer installed when
-          enabling Bluetooth via
-          <literal>hardware.bluetooth.enable</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>dump1090</literal> derivation has been changed to
-          use FlightAware's dump1090 as its upstream. However, this
-          version does not have an internal webserver anymore. The
-          assets in the <literal>share/dump1090</literal> directory of
-          the derivation can be used in conjunction with an external
-          webserver to replace this functionality.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The fourStore and fourStoreEndpoint modules have been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Polkit no longer has the user of uid 0 (root) as an admin
-          identity. We now follow the upstream default of only having
-          every member of the wheel group admin privileged. Before it
-          was root and members of wheel. The positive outcome of this is
-          pkexec GUI popups or terminal prompts will no longer require
-          the user to choose between two essentially equivalent choices
-          (whether to perform the action as themselves with wheel
-          permissions, or as the root user).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          NixOS containers no longer build NixOS manual by default. This
-          saves evaluation time, especially if there are many
-          declarative containers defined. Note that this is already done
-          when
-          <literal>&lt;nixos/modules/profiles/minimal.nix&gt;</literal>
-          module is included in container config.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>kresd</literal> services deprecates the
-          <literal>interfaces</literal> option in favor of the
-          <literal>listenPlain</literal> option which requires full
-          <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.socket.html#ListenStream=">systemd.socket
-          compatible</link> declaration which always include a port.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Virtual console options have been reorganized and can be found
-          under a single top-level attribute:
-          <literal>console</literal>. The full set of changes is as
-          follows:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>i18n.consoleFont</literal> renamed to
-              <link xlink:href="options.html#opt-console.font">console.font</link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>i18n.consoleKeyMap</literal> renamed to
-              <link xlink:href="options.html#opt-console.keyMap">console.keyMap</link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>i18n.consoleColors</literal> renamed to
-              <link xlink:href="options.html#opt-console.colors">console.colors</link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>i18n.consolePackages</literal> renamed to
-              <link xlink:href="options.html#opt-console.packages">console.packages</link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>i18n.consoleUseXkbConfig</literal> renamed to
-              <link xlink:href="options.html#opt-console.useXkbConfig">console.useXkbConfig</link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>boot.earlyVconsoleSetup</literal> renamed to
-              <link xlink:href="options.html#opt-console.earlySetup">console.earlySetup</link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>boot.extraTTYs</literal> renamed to
-              <literal>console.extraTTYs</literal>.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.awstats.enable">awstats</link>
-          module has been rewritten to serve stats via static html
-          pages, updated on a timer, over
-          <link xlink:href="options.html#opt-services.nginx.virtualHosts">nginx</link>,
-          instead of dynamic cgi pages over
-          <link xlink:href="options.html#opt-services.httpd.enable">apache</link>.
-        </para>
-        <para>
-          Minor changes will be required to migrate existing
-          configurations. Details of the required changes can seen by
-          looking through the
-          <link xlink:href="options.html#opt-services.awstats.enable">awstats</link>
-          module.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The httpd module no longer provides options to support serving
-          web content without defining a virtual host. As a result of
-          this the
-          <link xlink:href="options.html#opt-services.httpd.logPerVirtualHost">services.httpd.logPerVirtualHost</link>
-          option now defaults to <literal>true</literal> instead of
-          <literal>false</literal>. Please update your configuration to
-          make use of
-          <link xlink:href="options.html#opt-services.httpd.virtualHosts">services.httpd.virtualHosts</link>.
-        </para>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.httpd.virtualHosts">services.httpd.virtualHosts.&lt;name&gt;</link>
-          option has changed type from a list of submodules to an
-          attribute set of submodules, better matching
-          <link xlink:href="options.html#opt-services.nginx.virtualHosts">services.nginx.virtualHosts.&lt;name&gt;</link>.
-        </para>
-        <para>
-          This change comes with the addition of the following options
-          which mimic the functionality of their
-          <literal>nginx</literal> counterparts:
-          <link xlink:href="options.html#opt-services.httpd.virtualHosts">services.httpd.virtualHosts.&lt;name&gt;.addSSL</link>,
-          <link xlink:href="options.html#opt-services.httpd.virtualHosts">services.httpd.virtualHosts.&lt;name&gt;.forceSSL</link>,
-          <link xlink:href="options.html#opt-services.httpd.virtualHosts">services.httpd.virtualHosts.&lt;name&gt;.onlySSL</link>,
-          <link xlink:href="options.html#opt-services.httpd.virtualHosts">services.httpd.virtualHosts.&lt;name&gt;.enableACME</link>,
-          <link xlink:href="options.html#opt-services.httpd.virtualHosts">services.httpd.virtualHosts.&lt;name&gt;.acmeRoot</link>,
-          and
-          <link xlink:href="options.html#opt-services.httpd.virtualHosts">services.httpd.virtualHosts.&lt;name&gt;.useACMEHost</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          For NixOS configuration options, the <literal>loaOf</literal>
-          type has been deprecated and will be removed in a future
-          release. In nixpkgs, options of this type will be changed to
-          <literal>attrsOf</literal> instead. If you were using one of
-          these in your configuration, you will see a warning suggesting
-          what changes will be required.
-        </para>
-        <para>
-          For example,
-          <link xlink:href="options.html#opt-users.users">users.users</link>
-          is a <literal>loaOf</literal> option that is commonly used as
-          follows:
-        </para>
-        <programlisting language="bash">
-{
-  users.users =
-    [ { name = &quot;me&quot;;
-        description = &quot;My personal user.&quot;;
-        isNormalUser = true;
-      }
-    ];
-}
-</programlisting>
-        <para>
-          This should be rewritten by removing the list and using the
-          value of <literal>name</literal> as the name of the attribute
-          set:
-        </para>
-        <programlisting language="bash">
-{
-  users.users.me =
-    { description = &quot;My personal user.&quot;;
-      isNormalUser = true;
-    };
-}
-</programlisting>
-        <para>
-          For more information on this change have look at these links:
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/1800">issue
-          #1800</link>,
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/63103">PR
-          #63103</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          For NixOS modules, the types
-          <literal>types.submodule</literal> and
-          <literal>types.submoduleWith</literal> now support paths as
-          allowed values, similar to how <literal>imports</literal>
-          supports paths. Because of this, if you have a module that
-          defines an option of type
-          <literal>either (submodule ...) path</literal>, it will break
-          since a path is now treated as the first type instead of the
-          second. To fix this, change the type to
-          <literal>either path (submodule ...)</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.buildkite-agents">Buildkite
-          Agent</link> module and corresponding packages have been
-          updated to 3.x, and to support multiple instances of the agent
-          running at the same time. This means you will have to rename
-          <literal>services.buildkite-agent</literal> to
-          <literal>services.buildkite-agents.&lt;name&gt;</literal>.
-          Furthermore, the following options have been changed:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>services.buildkite-agent.meta-data</literal> has
-              been renamed to
-              <link xlink:href="options.html#opt-services.buildkite-agents">services.buildkite-agents.&lt;name&gt;.tags</link>,
-              to match upstreams naming for 3.x. Its type has also
-              changed - it now accepts an attrset of strings.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The<literal>services.buildkite-agent.openssh.publicKeyPath</literal>
-              option has been removed, as it's not necessary to deploy
-              public keys to clone private repositories.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.buildkite-agent.openssh.privateKeyPath</literal>
-              has been renamed to
-              <link xlink:href="options.html#opt-services.buildkite-agents">buildkite-agents.&lt;name&gt;.privateSshKeyPath</link>,
-              as the whole <literal>openssh</literal> now only contained
-              that single option.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.buildkite-agents">services.buildkite-agents.&lt;name&gt;.shell</link>
-              has been introduced, allowing to specify a custom shell to
-              be used.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>citrix_workspace_19_3_0</literal> package has
-          been removed as it will be EOLed within the lifespan of 20.03.
-          For further information, please refer to the
-          <link xlink:href="https://www.citrix.com/de-de/support/product-lifecycle/milestones/receiver.html">support
-          and maintenance information</link> from upstream.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>gcc5</literal> and <literal>gfortran5</literal>
-          packages have been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.xserver.displayManager.auto</literal>
-          module has been removed. It was only intended for use in
-          internal NixOS tests, and gave the false impression of it
-          being a special display manager when it's actually LightDM.
-          Please use the
-          <literal>services.xserver.displayManager.lightdm.autoLogin</literal>
-          options instead, or any other display manager in NixOS as they
-          all support auto-login. If you used this module specifically
-          because it permitted root auto-login you can override the
-          lightdm-autologin pam module like:
-        </para>
-        <programlisting language="bash">
-{
-  security.pam.services.lightdm-autologin.text = lib.mkForce ''
-      auth     requisite pam_nologin.so
-      auth     required  pam_succeed_if.so quiet
-      auth     required  pam_permit.so
-
-      account  include   lightdm
-
-      password include   lightdm
-
-      session  include   lightdm
-  '';
-}
-</programlisting>
-        <para>
-          The difference is the:
-        </para>
-        <programlisting>
-auth required pam_succeed_if.so quiet
-</programlisting>
-        <para>
-          line, where default it's:
-        </para>
-        <programlisting>
- auth required pam_succeed_if.so uid &gt;= 1000 quiet
-</programlisting>
-        <para>
-          not permitting users with uid's below 1000 (like root). All
-          other display managers in NixOS are configured like this.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          There have been lots of improvements to the Mailman module. As
-          a result,
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              The <literal>services.mailman.hyperkittyBaseUrl</literal>
-              option has been renamed to
-              <link xlink:href="options.html#opt-services.mailman.hyperkitty.baseUrl">services.mailman.hyperkitty.baseUrl</link>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The <literal>services.mailman.hyperkittyApiKey</literal>
-              option has been removed. This is because having an option
-              for the Hyperkitty API key meant that the API key would be
-              stored in the world-readable Nix store, which was a
-              security vulnerability. A new Hyperkitty API key will be
-              generated the first time the new Hyperkitty service is
-              run, and it will then be persisted outside of the Nix
-              store. To continue using Hyperkitty, you must set
-              <link xlink:href="options.html#opt-services.mailman.hyperkitty.enable">services.mailman.hyperkitty.enable</link>
-              to <literal>true</literal>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Additionally, some Postfix configuration must now be set
-              manually instead of automatically by the Mailman module:
-            </para>
-            <programlisting language="bash">
-{
-  services.postfix.relayDomains = [ &quot;hash:/var/lib/mailman/data/postfix_domains&quot; ];
-  services.postfix.config.transport_maps = [ &quot;hash:/var/lib/mailman/data/postfix_lmtp&quot; ];
-  services.postfix.config.local_recipient_maps = [ &quot;hash:/var/lib/mailman/data/postfix_lmtp&quot; ];
-}
-</programlisting>
-            <para>
-              This is because some users may want to include other
-              values in these lists as well, and this was not possible
-              if they were set automatically by the Mailman module. It
-              would not have been possible to just concatenate values
-              from multiple modules each setting the values they needed,
-              because the order of elements in the list is significant.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The LLVM versions 3.5, 3.9 and 4 (including the corresponding
-          CLang versions) have been dropped.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <literal>networking.interfaces.*.preferTempAddress</literal>
-          option has been replaced by
-          <literal>networking.interfaces.*.tempAddress</literal>. The
-          new option allows better control of the IPv6 temporary
-          addresses, including completely disabling them for interfaces
-          where they are not needed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Rspamd was updated to version 2.2. Read
-          <link xlink:href="https://rspamd.com/doc/migration.html#migration-to-rspamd-20">
-          the upstream migration notes</link> carefully. Please be
-          especially aware that some modules were removed and the
-          default Bayes backend is now Redis.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>*psu</literal> versions of oraclejdk8 have been
-          removed as they aren't provided by upstream anymore.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.dnscrypt-proxy</literal> module has been
-          removed as it used the deprecated version of dnscrypt-proxy.
-          We've added
-          <link xlink:href="options.html#opt-services.dnscrypt-proxy2.enable">services.dnscrypt-proxy2.enable</link>
-          to use the supported version. This module supports
-          configuration via the Nix attribute set
-          <link xlink:href="options.html#opt-services.dnscrypt-proxy2.settings">services.dnscrypt-proxy2.settings</link>,
-          or by passing a TOML configuration file via
-          <link xlink:href="options.html#opt-services.dnscrypt-proxy2.configFile">services.dnscrypt-proxy2.configFile</link>.
-        </para>
-        <programlisting language="bash">
-{
-  # Example configuration:
-  services.dnscrypt-proxy2.enable = true;
-  services.dnscrypt-proxy2.settings = {
-    listen_addresses = [ &quot;127.0.0.1:43&quot; ];
-    sources.public-resolvers = {
-      urls = [ &quot;https://download.dnscrypt.info/resolvers-list/v2/public-resolvers.md&quot; ];
-      cache_file = &quot;public-resolvers.md&quot;;
-      minisign_key = &quot;RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3&quot;;
-      refresh_delay = 72;
-    };
-  };
-
-  services.dnsmasq.enable = true;
-  services.dnsmasq.servers = [ &quot;127.0.0.1#43&quot; ];
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>qesteidutil</literal> has been deprecated in favor of
-          <literal>qdigidoc</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          sqldeveloper_18 has been removed as it's not maintained
-          anymore, sqldeveloper has been updated to version
-          <literal>19.4</literal>. Please note that this means that this
-          means that the oraclejdk is now required. For further
-          information please read the
-          <link xlink:href="https://www.oracle.com/technetwork/developer-tools/sql-developer/downloads/sqldev-relnotes-194-5908846.html">release
-          notes</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Haskell <literal>env</literal> and <literal>shellFor</literal>
-          dev shell environments now organize dependencies the same way
-          as regular builds. In particular, rather than receiving all
-          the different lists of dependencies mashed together as one big
-          list, and then partitioning into Haskell and non-Hakell
-          dependencies, they work from the original many different
-          dependency parameters and don't need to algorithmically
-          partition anything.
-        </para>
-        <para>
-          This means that if you incorrectly categorize a dependency,
-          e.g. non-Haskell library dependency as a
-          <literal>buildDepends</literal> or run-time Haskell dependency
-          as a <literal>setupDepends</literal>, whereas things would
-          have worked before they may not work now.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The gcc-snapshot-package has been removed. It's marked as
-          broken for &gt;2 years and used to point to a fairly old
-          snapshot from the gcc7-branch.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The nixos-build-vms8 -script now uses the python test-driver.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The riot-web package now accepts configuration overrides as an
-          attribute set instead of a string. A formerly used JSON
-          configuration can be converted to an attribute set with
-          <literal>builtins.fromJSON</literal>.
-        </para>
-        <para>
-          The new default configuration also disables automatic guest
-          account registration and analytics to improve privacy. The
-          previous behavior can be restored by setting
-          <literal>config.riot-web.conf = { disable_guests = false; piwik = true; }</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Stand-alone usage of <literal>Upower</literal> now requires
-          <literal>services.upower.enable</literal> instead of just
-          installing into
-          <link xlink:href="options.html#opt-environment.systemPackages">environment.systemPackages</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          nextcloud has been updated to <literal>v18.0.2</literal>. This
-          means that users from NixOS 19.09 can't upgrade directly since
-          you can only move one version forward and 19.09 uses
-          <literal>v16.0.8</literal>.
-        </para>
-        <para>
-          To provide a safe upgrade-path and to circumvent similar
-          issues in the future, the following measures were taken:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              The pkgs.nextcloud-attribute has been removed and replaced
-              with versioned attributes (currently pkgs.nextcloud17 and
-              pkgs.nextcloud18). With this change major-releases can be
-              backported without breaking stuff and to make
-              upgrade-paths easier.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Existing setups will be detected using
-              <link xlink:href="options.html#opt-system.stateVersion">system.stateVersion</link>:
-              by default, nextcloud17 will be used, but will raise a
-              warning which notes that after that deploy it's
-              recommended to update to the latest stable version
-              (nextcloud18) by declaring the newly introduced setting
-              <link xlink:href="options.html#opt-services.nextcloud.package">services.nextcloud.package</link>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Users with an overlay (e.g. to use nextcloud at version
-              <literal>v18</literal> on <literal>19.09</literal>) will
-              get an evaluation error by default. This is done to ensure
-              that our
-              <link xlink:href="options.html#opt-services.nextcloud.package">package</link>-option
-              doesn't select an older version by accident. It's
-              recommended to use pkgs.nextcloud18 or to set
-              <link xlink:href="options.html#opt-services.nextcloud.package">package</link>
-              to pkgs.nextcloud explicitly.
-            </para>
-          </listitem>
-        </itemizedlist>
-        <warning>
-          <para>
-            Please note that if you're coming from
-            <literal>19.03</literal> or older, you have to manually
-            upgrade to <literal>19.09</literal> first to upgrade your
-            server to Nextcloud v16.
-          </para>
-        </warning>
-      </listitem>
-      <listitem>
-        <para>
-          Hydra has gained a massive performance improvement due to
-          <link xlink:href="https://github.com/NixOS/hydra/pull/710">some
-          database schema changes</link> by adding several IDs and
-          better indexing. However, it's necessary to upgrade Hydra in
-          multiple steps:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              At first, an older version of Hydra needs to be deployed
-              which adds those (nullable) columns. When having set
-              <link xlink:href="options.html#opt-system.stateVersion">stateVersion
-              </link> to a value older than <literal>20.03</literal>,
-              this package will be selected by default from the module
-              when upgrading. Otherwise, the package can be deployed
-              using the following config:
-            </para>
-            <programlisting language="bash">
-{ pkgs, ... }: {
-  services.hydra.package = pkgs.hydra-migration;
-}
-</programlisting>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Automatically fill the newly added ID columns on the server by
-          running the following command:
-        </para>
-        <programlisting>
-$ hydra-backfill-ids
-</programlisting>
-        <warning>
-          <para>
-            Please note that this process can take a while depending on
-            your database-size!
-          </para>
-        </warning>
-      </listitem>
-      <listitem>
-        <para>
-          Deploy a newer version of Hydra to activate the DB
-          optimizations. This can be done by using hydra-unstable. This
-          package already includes
-          <link xlink:href="https://github.com/nixos/rfcs/pull/49">flake-support</link>
-          and is therefore compiled against pkgs.nixFlakes.
-        </para>
-        <warning>
-          <para>
-            If your
-            <link xlink:href="options.html#opt-system.stateVersion">stateVersion</link>
-            is set to <literal>20.03</literal> or greater,
-            hydra-unstable will be used automatically! This will break
-            your setup if you didn't run the migration.
-          </para>
-        </warning>
-        <para>
-          Please note that Hydra is currently not available with
-          nixStable as this doesn't compile anymore.
-        </para>
-        <warning>
-          <para>
-            pkgs.hydra has been removed to ensure a graceful
-            database-migration using the dedicated package-attributes.
-            If you still have pkgs.hydra defined in e.g. an overlay, an
-            assertion error will be thrown. To circumvent this, you need
-            to set
-            <link xlink:href="options.html#opt-services.hydra.package">services.hydra.package</link>
-            to pkgs.hydra explicitly and make sure you know what you're
-            doing!
-          </para>
-        </warning>
-      </listitem>
-      <listitem>
-        <para>
-          The TokuDB storage engine will be disabled in mariadb 10.5. It
-          is recommended to switch to RocksDB. See also
-          <link xlink:href="https://mariadb.com/kb/en/tokudb/">TokuDB</link>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-20.03-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          SD images are now compressed by default using
-          <literal>bzip2</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The nginx web server previously started its master process as
-          root privileged, then ran worker processes as a less
-          privileged identity user (the <literal>nginx</literal> user).
-          This was changed to start all of nginx as a less privileged
-          user (defined by <literal>services.nginx.user</literal> and
-          <literal>services.nginx.group</literal>). As a consequence,
-          all files that are needed for nginx to run (included
-          configuration fragments, SSL certificates and keys, etc.) must
-          now be readable by this less privileged user/group.
-        </para>
-        <para>
-          To continue to use the old approach, you can configure:
-        </para>
-        <programlisting language="bash">
-{
-  services.nginx.appendConfig = let cfg = config.services.nginx; in ''user ${cfg.user} ${cfg.group};'';
-  systemd.services.nginx.serviceConfig.User = lib.mkForce &quot;root&quot;;
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          OpenSSH has been upgraded from 7.9 to 8.1, improving security
-          and adding features but with potential incompatibilities.
-          Consult the
-          <link xlink:href="https://www.openssh.com/txt/release-8.1">
-          release announcement</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>PRETTY_NAME</literal> in
-          <literal>/etc/os-release</literal> now uses the short rather
-          than full version string.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The ACME module has switched from simp-le to
-          <link xlink:href="https://github.com/go-acme/lego">lego</link>
-          which allows us to support DNS-01 challenges and wildcard
-          certificates. The following options have been added:
-          <link xlink:href="options.html#opt-security.acme.acceptTerms">security.acme.acceptTerms</link>,
-          <link xlink:href="options.html#opt-security.acme.certs">security.acme.certs.&lt;name&gt;.dnsProvider</link>,
-          <link xlink:href="options.html#opt-security.acme.certs">security.acme.certs.&lt;name&gt;.credentialsFile</link>,
-          <link xlink:href="options.html#opt-security.acme.certs">security.acme.certs.&lt;name&gt;.dnsPropagationCheck</link>.
-          As well as this, the options
-          <literal>security.acme.acceptTerms</literal> and either
-          <literal>security.acme.email</literal> or
-          <literal>security.acme.certs.&lt;name&gt;.email</literal> must
-          be set in order to use the ACME module. Certificates will be
-          regenerated on activation, no account or certificate will be
-          migrated from simp-le. In particular private keys will not be
-          preserved. However, the credentials for simp-le are preserved
-          and thus it is possible to roll back to previous versions
-          without breaking certificate generation. Note also that in
-          contrary to simp-le a new private key is recreated at each
-          renewal by default, which can have consequences if you embed
-          your public key in apps.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          It is now possible to unlock LUKS-Encrypted file systems using
-          a FIDO2 token via
-          <literal>boot.initrd.luks.fido2Support</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Predictably named network interfaces get renamed in stage-1.
-          This means that it is possible to use the proper interface
-          name for e.g. Dropbear setups.
-        </para>
-        <para>
-          For further reference, please read
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/68953">#68953</link>
-          or the corresponding
-          <link xlink:href="https://discourse.nixos.org/t/predictable-network-interface-names-in-initrd/4055">discourse
-          thread</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The matrix-synapse-package has been updated to
-          <link xlink:href="https://github.com/matrix-org/synapse/releases/tag/v1.11.1">v1.11.1</link>.
-          Due to
-          <link xlink:href="https://github.com/matrix-org/synapse/releases/tag/v1.10.0rc1">stricter
-          requirements</link> for database configuration when using
-          postgresql, the automated database setup of the module has
-          been removed to avoid any further edge-cases.
-        </para>
-        <para>
-          matrix-synapse expects <literal>postgresql</literal>-databases
-          to have the options <literal>LC_COLLATE</literal> and
-          <literal>LC_CTYPE</literal> set to
-          <link xlink:href="https://www.postgresql.org/docs/12/locale.html"><literal>'C'</literal></link>
-          which basically instructs <literal>postgresql</literal> to
-          ignore any locale-based preferences.
-        </para>
-        <para>
-          Depending on your setup, you need to incorporate one of the
-          following changes in your setup to upgrade to 20.03:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              If you use <literal>sqlite3</literal> you don't need to do
-              anything.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              If you use <literal>postgresql</literal> on a different
-              server, you don't need to change anything as well since
-              this module was never designed to configure remote
-              databases.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              If you use <literal>postgresql</literal> and configured
-              your synapse initially on <literal>19.09</literal> or
-              older, you simply need to enable postgresql-support
-              explicitly:
-            </para>
-            <programlisting language="bash">
-{ ... }: {
-  services.matrix-synapse = {
-    enable = true;
-    /* and all the other config you've defined here */
-  };
-  services.postgresql.enable = true;
-}
-</programlisting>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          If you deploy a fresh matrix-synapse, you need to configure
-          the database yourself (e.g. by using the
-          <link xlink:href="options.html#opt-services.postgresql.initialScript">services.postgresql.initialScript</link>
-          option). An example for this can be found in the
-          <link linkend="module-services-matrix">documentation of the
-          Matrix module</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          If you initially deployed your matrix-synapse on
-          <literal>nixos-unstable</literal> <emphasis>after</emphasis>
-          the <literal>19.09</literal>-release, your database is
-          misconfigured due to a regression in NixOS. For now,
-          matrix-synapse will startup with a warning, but it's
-          recommended to reconfigure the database to set the values
-          <literal>LC_COLLATE</literal> and <literal>LC_CTYPE</literal>
-          to
-          <link xlink:href="https://www.postgresql.org/docs/12/locale.html"><literal>'C'</literal></link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-systemd.network.links">systemd.network.links</link>
-          option is now respected even when
-          <link xlink:href="options.html#opt-systemd.network.enable">systemd-networkd</link>
-          is disabled. This mirrors the behaviour of systemd - It's udev
-          that parses <literal>.link</literal> files, not
-          <literal>systemd-networkd</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          mongodb has been updated to version <literal>3.4.24</literal>.
-        </para>
-        <warning>
-          <para>
-            Please note that mongodb has been relicensed under their own
-            <link xlink:href="https://www.mongodb.com/licensing/server-side-public-license/faq"><literal> sspl</literal></link>-license.
-            Since it's not entirely free and not OSI-approved, it's
-            listed as non-free. This means that Hydra doesn't provide
-            prebuilt mongodb-packages and needs to be built locally.
-          </para>
-        </warning>
-      </listitem>
-    </itemizedlist>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2009.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2009.section.xml
deleted file mode 100644
index edebd92b327..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-2009.section.xml
+++ /dev/null
@@ -1,2210 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-20.09">
-  <title>Release 20.09 (<quote>Nightingale</quote>, 2020.10/27)</title>
-  <para>
-    Support is planned until the end of June 2021, handing over to
-    21.05. (Plans
-    <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md#core-changes">
-    have shifted</link> by two months since release of 20.09.)
-  </para>
-  <section xml:id="sec-release-20.09-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to 7349 new, 14442 updated, and 8181 removed packages,
-      this release has the following highlights:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Core version changes:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              gcc: 9.2.0 -&gt; 9.3.0
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              glibc: 2.30 -&gt; 2.31
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              linux: still defaults to 5.4.x, all supported kernels
-              available
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              mesa: 19.3.5 -&gt; 20.1.7
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Desktop Environments:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              plasma5: 5.17.5 -&gt; 5.18.5
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              kdeApplications: 19.12.3 -&gt; 20.08.1
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              gnome3: 3.34 -&gt; 3.36, see its
-              <link xlink:href="https://help.gnome.org/misc/release-notes/3.36/">release
-              notes</link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              cinnamon: added at 4.6
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              NixOS now distributes an official
-              <link xlink:href="https://nixos.org/download.html#nixos-iso">GNOME
-              ISO</link>
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Programming Languages and Frameworks:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              Agda ecosystem was heavily reworked (see more details
-              below)
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              PHP now defaults to PHP 7.4, updated from 7.3
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              PHP 7.2 is no longer supported due to upstream not
-              supporting this version for the entire lifecycle of the
-              20.09 release
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Python 3 now defaults to Python 3.8 instead of 3.7
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Python 3.5 reached its upstream EOL at the end of
-              September 2020: it has been removed from the list of
-              available packages
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Databases and Service Monitoring:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              MariaDB has been updated to 10.4, MariaDB Galera to 26.4.
-              Please read the related upgrade instructions under
-              <link linkend="sec-release-20.09-incompatibilities">backwards
-              incompatibilities</link> before upgrading.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Zabbix now defaults to 5.0, updated from 4.4. Please read
-              related sections under
-              <link linkend="sec-release-20.09-incompatibilities">backwards
-              compatibilities</link> before upgrading.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Major module changes:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              Quickly configure a complete, private, self-hosted video
-              conferencing solution with the new Jitsi Meet module.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Two new options,
-              <link xlink:href="options.html#opt-services.openssh.authorizedKeysCommand">authorizedKeysCommand</link>
-              and
-              <link xlink:href="options.html#opt-services.openssh.authorizedKeysCommandUser">authorizedKeysCommandUser</link>,
-              have been added to the <literal>openssh</literal> module.
-              If you have <literal>AuthorizedKeysCommand</literal> in
-              your
-              <link xlink:href="options.html#opt-services.openssh.extraConfig">services.openssh.extraConfig</link>
-              you should make use of these new options instead.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              There is a new module for Podman
-              (<literal>virtualisation.podman</literal>), a drop-in
-              replacement for the Docker command line.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The new <literal>virtualisation.containers</literal>
-              module manages configuration shared by the CRI-O and
-              Podman modules.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Declarative Docker containers are renamed from
-              <literal>docker-containers</literal> to
-              <literal>virtualisation.oci-containers.containers</literal>.
-              This is to make it possible to use
-              <literal>podman</literal> instead of
-              <literal>docker</literal>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The new option
-              <link xlink:href="options.html#opt-documentation.man.generateCaches">documentation.man.generateCaches</link>
-              has been added to automatically generate the
-              <literal>man-db</literal> caches, which are needed by
-              utilities like <literal>whatis</literal> and
-              <literal>apropos</literal>. The caches are generated
-              during the build of the NixOS configuration: since this
-              can be expensive when a large number of packages are
-              installed, the feature is disabled by default.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>services.postfix.sslCACert</literal> was replaced
-              by
-              <literal>services.postfix.tlsTrustedAuthorities</literal>
-              which now defaults to system certificate authorities.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The various documented workarounds to use steam have been
-              converted to a module.
-              <literal>programs.steam.enable</literal> enables steam,
-              controller support and the workarounds.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Support for built-in LCDs in various pieces of Logitech
-              hardware (keyboards and USB speakers).
-              <literal>hardware.logitech.lcd.enable</literal> enables
-              support for all hardware supported by the
-              <link xlink:href="https://sourceforge.net/projects/g15daemon/">g15daemon
-              project</link>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The GRUB module gained support for basic password
-              protection, which allows to restrict non-default entries
-              in the boot menu to one or more users. The users and
-              passwords are defined via the option
-              <literal>boot.loader.grub.users</literal>. Note: Password
-              support is only available in GRUB version 2.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          NixOS module changes:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              The NixOS module system now supports freeform modules as a
-              mix between <literal>types.attrsOf</literal> and
-              <literal>types.submodule</literal>. These allow you to
-              explicitly declare a subset of options while still
-              permitting definitions without an associated option. See
-              <xref linkend="sec-freeform-modules" /> for how to use
-              them.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Following its deprecation in 20.03, the Perl NixOS test
-              driver has been removed. All remaining tests have been
-              ported to the Python test framework. Code outside nixpkgs
-              using <literal>make-test.nix</literal> or
-              <literal>testing.nix</literal> needs to be ported to
-              <literal>make-test-python.nix</literal> and
-              <literal>testing-python.nix</literal> respectively.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Subordinate GID and UID mappings are now set up
-              automatically for all normal users. This will make
-              container tools like Podman work as non-root users out of
-              the box.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Starting with this release, the hydra-build-result
-          <literal>nixos-YY.MM</literal> branches no longer exist in the
-          <link xlink:href="https://github.com/nixos/nixpkgs-channels">deprecated
-          nixpkgs-channels repository</link>. These branches are now in
-          <link xlink:href="https://github.com/nixos/nixpkgs">the main
-          nixpkgs repository</link>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-20.09-new-services">
-    <title>New Services</title>
-    <para>
-      In addition to 1119 new, 118 updated, and 476 removed options; 61
-      new modules were added since the last release:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Hardware:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-hardware.system76.firmware-daemon.enable">hardware.system76.firmware-daemon.enable</link>
-              adds easy support of system76 firmware
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-hardware.uinput.enable">hardware.uinput.enable</link>
-              loads uinput kernel module
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-hardware.video.hidpi.enable">hardware.video.hidpi.enable</link>
-              enable good defaults for HiDPI displays
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-hardware.wooting.enable">hardware.wooting.enable</link>
-              support for Wooting keyboards
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-hardware.xpadneo.enable">hardware.xpadneo.enable</link>
-              xpadneo driver for Xbox One wireless controllers
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Programs:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-programs.hamster.enable">programs.hamster.enable</link>
-              enable hamster time tracking
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-programs.steam.enable">programs.steam.enable</link>
-              adds easy enablement of steam and related system
-              configuration
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Security:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-security.doas.enable">security.doas.enable</link>
-              alternative to sudo, allows non-root users to execute
-              commands as root
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-security.tpm2.enable">security.tpm2.enable</link>
-              add Trusted Platform Module 2 support
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          System:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-boot.initrd.network.openvpn.enable">boot.initrd.network.openvpn.enable</link>
-              start an OpenVPN client during initrd boot
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Virtualization:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-boot.enableContainers">boot.enableContainers</link>
-              use nixos-containers
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-virtualisation.oci-containers.containers">virtualisation.oci-containers.containers</link>
-              run OCI (Docker) containers
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-virtualisation.podman.enable">virtualisation.podman.enable</link>
-              daemonless container engine
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Services:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.ankisyncd.enable">services.ankisyncd.enable</link>
-              Anki sync server
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.bazarr.enable">services.bazarr.enable</link>
-              Subtitle manager for Sonarr and Radarr
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.biboumi.enable">services.biboumi.enable</link>
-              Biboumi XMPP gateway to IRC
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.blockbook-frontend">services.blockbook-frontend</link>
-              Blockbook-frontend, a service for the Trezor wallet
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.cage.enable">services.cage.enable</link>
-              Wayland cage service
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.convos.enable">services.convos.enable</link>
-              IRC daemon, which can be accessed throught the browser
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.engelsystem.enable">services.engelsystem.enable</link>
-              Tool for coordinating volunteers and shifts on large
-              events
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.espanso.enable">services.espanso.enable</link>
-              text-expander written in rust
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.foldingathome.enable">services.foldingathome.enable</link>
-              Folding@home client
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.gerrit.enable">services.gerrit.enable</link>
-              Web-based team code collaboration tool
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.go-neb.enable">services.go-neb.enable</link>
-              Matrix bot
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.hardware.xow.enable">services.hardware.xow.enable</link>
-              xow as a systemd service
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.hercules-ci-agent.enable">services.hercules-ci-agent.enable</link>
-              Hercules CI build agent
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.jicofo.enable">services.jicofo.enable</link>
-              Jitsi Conference Focus, component of Jitsi Meet
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.jirafeau.enable">services.jirafeau.enable</link>
-              A web file repository
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.jitsi-meet.enable">services.jitsi-meet.enable</link>
-              Secure, simple and scalable video conferences
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.jitsi-videobridge.enable">services.jitsi-videobridge.enable</link>
-              Jitsi Videobridge, a WebRTC compatible router
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.jupyterhub.enable">services.jupyterhub.enable</link>
-              Jupyterhub development server
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.k3s.enable">services.k3s.enable</link>
-              Lightweight Kubernetes distribution
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.magic-wormhole-mailbox-server.enable">services.magic-wormhole-mailbox-server.enable</link>
-              Magic Wormhole Mailbox Server
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.malcontent.enable">services.malcontent.enable</link>
-              Parental Control support
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.matrix-appservice-discord.enable">services.matrix-appservice-discord.enable</link>
-              Matrix and Discord bridge
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.mautrix-telegram.enable">services.mautrix-telegram.enable</link>
-              Matrix-Telegram puppeting/relaybot bridge
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.mirakurun.enable">services.mirakurun.enable</link>
-              Japanese DTV Tuner Server Service
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.molly-brown.enable">services.molly-brown.enable</link>
-              Molly-Brown Gemini server
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.mullvad-vpn.enable">services.mullvad-vpn.enable</link>
-              Mullvad VPN daemon
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.ncdns.enable">services.ncdns.enable</link>
-              Namecoin to DNS bridge
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.nextdns.enable">services.nextdns.enable</link>
-              NextDNS to DoH Proxy service
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.nix-store-gcs-proxy">services.nix-store-gcs-proxy</link>
-              Google storage bucket to be used as a nix store
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.onedrive.enable">services.onedrive.enable</link>
-              OneDrive sync service
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.pinnwand.enable">services.pinnwand.enable</link>
-              Pastebin-like service
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.pixiecore.enable">services.pixiecore.enable</link>
-              Manage network booting of machines
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.privacyidea.enable">services.privacyidea.enable</link>
-              Privacy authentication server
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.quorum.enable">services.quorum.enable</link>
-              Quorum blockchain daemon
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.robustirc-bridge.enable">services.robustirc-bridge.enable</link>
-              RobustIRC bridge
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.rss-bridge.enable">services.rss-bridge.enable</link>
-              Generate RSS and Atom feeds
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.rtorrent.enable">services.rtorrent.enable</link>
-              rTorrent service
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.smartdns.enable">services.smartdns.enable</link>
-              SmartDNS DNS server
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.sogo.enable">services.sogo.enable</link>
-              SOGo groupware
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.teeworlds.enable">services.teeworlds.enable</link>
-              Teeworlds game server
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.torque.mom.enable">services.torque.mom.enable</link>
-              torque computing node
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.torque.server.enable">services.torque.server.enable</link>
-              torque server
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.tuptime.enable">services.tuptime.enable</link>
-              A total uptime service
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.urserver.enable">services.urserver.enable</link>
-              X11 remote server
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.wasabibackend.enable">services.wasabibackend.enable</link>
-              Wasabi backend service
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.yubikey-agent.enable">services.yubikey-agent.enable</link>
-              Yubikey agent
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-services.zigbee2mqtt.enable">services.zigbee2mqtt.enable</link>
-              Zigbee to MQTT bridge
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-20.09-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <para>
-      When upgrading from a previous release, please be aware of the
-      following incompatible changes:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          MariaDB has been updated to 10.4, MariaDB Galera to 26.4.
-          Before you upgrade, it would be best to take a backup of your
-          database. For MariaDB Galera Cluster, see
-          <link xlink:href="https://mariadb.com/kb/en/upgrading-from-mariadb-103-to-mariadb-104-with-galera-cluster/">Upgrading
-          from MariaDB 10.3 to MariaDB 10.4 with Galera Cluster</link>
-          instead. Before doing the upgrade read
-          <link xlink:href="https://mariadb.com/kb/en/upgrading-from-mariadb-103-to-mariadb-104/#incompatible-changes-between-103-and-104">Incompatible
-          Changes Between 10.3 and 10.4</link>. After the upgrade you
-          will need to run <literal>mysql_upgrade</literal>. MariaDB
-          10.4 introduces a number of changes to the authentication
-          process, intended to make things easier and more intuitive.
-          See
-          <link xlink:href="https://mariadb.com/kb/en/authentication-from-mariadb-104/">Authentication
-          from MariaDB 10.4</link>. unix_socket auth plugin does not use
-          a password, and uses the connecting user's UID instead. When a
-          new MariaDB data directory is initialized, two MariaDB users
-          are created and can be used with new unix_socket auth plugin,
-          as well as traditional mysql_native_password plugin:
-          root@localhost and mysql@localhost. To actually use the
-          traditional mysql_native_password plugin method, one must run
-          the following:
-        </para>
-        <programlisting language="bash">
-{
-services.mysql.initialScript = pkgs.writeText &quot;mariadb-init.sql&quot; ''
-  ALTER USER root@localhost IDENTIFIED VIA mysql_native_password USING PASSWORD(&quot;verysecret&quot;);
-'';
-}
-</programlisting>
-        <para>
-          When MariaDB data directory is just upgraded (not
-          initialized), the users are not created or modified.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          MySQL server is now started with additional systemd
-          sandbox/hardening options for better security. The PrivateTmp,
-          ProtectHome, and ProtectSystem options may be problematic when
-          MySQL is attempting to read from or write to your filesystem
-          anywhere outside of its own state directory, for example when
-          calling
-          <literal>LOAD DATA INFILE or SELECT * INTO OUTFILE</literal>.
-          In this scenario a variant of the following may be required: -
-          allow MySQL to read from /home and /tmp directories when using
-          <literal>LOAD DATA INFILE</literal>
-        </para>
-        <programlisting language="bash">
-{
-  systemd.services.mysql.serviceConfig.ProtectHome = lib.mkForce &quot;read-only&quot;;
-}
-</programlisting>
-        <para>
-          - allow MySQL to write to custom folder
-          <literal>/var/data</literal> when using
-          <literal>SELECT * INTO OUTFILE</literal>, assuming the mysql
-          user has write access to <literal>/var/data</literal>
-        </para>
-        <programlisting language="bash">
-{
-  systemd.services.mysql.serviceConfig.ReadWritePaths = [ &quot;/var/data&quot; ];
-}
-</programlisting>
-        <para>
-          The MySQL service no longer runs its
-          <literal>systemd</literal> service startup script as
-          <literal>root</literal> anymore. A dedicated non
-          <literal>root</literal> super user account is required for
-          operation. This means users with an existing MySQL or MariaDB
-          database server are required to run the following SQL
-          statements as a super admin user before upgrading:
-        </para>
-        <programlisting language="SQL">
-CREATE USER IF NOT EXISTS 'mysql'@'localhost' identified with unix_socket;
-GRANT ALL PRIVILEGES ON *.* TO 'mysql'@'localhost' WITH GRANT OPTION;
-</programlisting>
-        <para>
-          If you use MySQL instead of MariaDB please replace
-          <literal>unix_socket</literal> with
-          <literal>auth_socket</literal>. If you have changed the value
-          of
-          <link xlink:href="options.html#opt-services.mysql.user">services.mysql.user</link>
-          from the default of <literal>mysql</literal> to a different
-          user please change <literal>'mysql'@'localhost'</literal> to
-          the corresponding user instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Zabbix now defaults to 5.0, updated from 4.4. Please carefully
-          read through
-          <link xlink:href="https://www.zabbix.com/documentation/current/manual/installation/upgrade/sources">the
-          upgrade guide</link> and apply any changes required. Be sure
-          to take special note of the section on
-          <link xlink:href="https://www.zabbix.com/documentation/current/manual/installation/upgrade_notes_500#enabling_extended_range_of_numeric_float_values">enabling
-          extended range of numeric (float) values</link> as you will
-          need to apply this database migration manually.
-        </para>
-        <para>
-          If you are using Zabbix Server with a MySQL or MariaDB
-          database you should note that using a character set of
-          <literal>utf8</literal> and a collate of
-          <literal>utf8_bin</literal> has become mandatory with this
-          release. See the upstream
-          <link xlink:href="https://support.zabbix.com/browse/ZBX-17357">issue</link>
-          for further discussion. Before upgrading you should check the
-          character set and collation used by your database and ensure
-          they are correct:
-        </para>
-        <programlisting language="SQL">
-SELECT
-  default_character_set_name,
-  default_collation_name
-FROM
-  information_schema.schemata
-WHERE
-  schema_name = 'zabbix';
-</programlisting>
-        <para>
-          If these values are not correct you should take a backup of
-          your database and convert the character set and collation as
-          required. Here is an
-          <link xlink:href="https://www.zabbix.com/forum/zabbix-help/396573-reinstall-after-upgrade?p=396891#post396891">example</link>
-          of how to do so, taken from the Zabbix forums:
-        </para>
-        <programlisting language="SQL">
-ALTER DATABASE `zabbix` DEFAULT CHARACTER SET utf8 COLLATE utf8_bin;
-
--- the following will produce a list of SQL commands you should subsequently execute
-SELECT CONCAT(&quot;ALTER TABLE &quot;, TABLE_NAME,&quot; CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin;&quot;) AS ExecuteTheString
-FROM information_schema.`COLUMNS`
-WHERE table_schema = &quot;zabbix&quot; AND COLLATION_NAME = &quot;utf8_general_ci&quot;;
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          maxx package removed along with
-          <literal>services.xserver.desktopManager.maxx</literal>
-          module. Please migrate to cdesktopenv and
-          <literal>services.xserver.desktopManager.cde</literal> module.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.matrix-synapse.enable">matrix-synapse</link>
-          module no longer includes optional dependencies by default,
-          they have to be added through the
-          <link xlink:href="options.html#opt-services.matrix-synapse.plugins">plugins</link>
-          option.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>buildGoModule</literal> now internally creates a
-          vendor directory in the source tree for downloaded modules
-          instead of using go's
-          <link xlink:href="https://golang.org/cmd/go/#hdr-Module_proxy_protocol">module
-          proxy protocol</link>. This storage format is simpler and
-          therefore less likely to break with future versions of go. As
-          a result <literal>buildGoModule</literal> switched from
-          <literal>modSha256</literal> to the
-          <literal>vendorSha256</literal> attribute to pin fetched
-          version data.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Grafana is now built without support for phantomjs by default.
-          Phantomjs support has been
-          <link xlink:href="https://grafana.com/docs/grafana/latest/guides/whats-new-in-v6-4/">deprecated
-          in Grafana</link> and the phantomjs project is
-          <link xlink:href="https://github.com/ariya/phantomjs/issues/15344#issue-302015362">currently
-          unmaintained</link>. It can still be enabled by providing
-          <literal>phantomJsSupport = true</literal> to the package
-          instantiation:
-        </para>
-        <programlisting language="bash">
-{
-  services.grafana.package = pkgs.grafana.overrideAttrs (oldAttrs: rec {
-    phantomJsSupport = true;
-  });
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.supybot.enable">supybot</link>
-          module now uses <literal>/var/lib/supybot</literal> as its
-          default
-          <link xlink:href="options.html#opt-services.supybot.stateDir">stateDir</link>
-          path if <literal>stateVersion</literal> is 20.09 or higher. It
-          also enables a number of
-          <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Sandboxing">systemd
-          sandboxing options</link> which may possibly interfere with
-          some plugins. If this is the case you can disable the options
-          through attributes in
-          <literal>systemd.services.supybot.serviceConfig</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>security.duosec.skey</literal> option, which
-          stored a secret in the nix store, has been replaced by a new
-          <link xlink:href="options.html#opt-security.duosec.secretKeyFile">security.duosec.secretKeyFile</link>
-          option for better security.
-        </para>
-        <para>
-          <literal>security.duosec.ikey</literal> has been renamed to
-          <link xlink:href="options.html#opt-security.duosec.integrationKey">security.duosec.integrationKey</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>vmware</literal> has been removed from the
-          <literal>services.x11.videoDrivers</literal> defaults. For
-          VMWare guests set
-          <literal>virtualisation.vmware.guest.enable</literal> to
-          <literal>true</literal> which will include the appropriate
-          drivers.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The initrd SSH support now uses OpenSSH rather than Dropbear
-          to allow the use of Ed25519 keys and other OpenSSH-specific
-          functionality. Host keys must now be in the OpenSSH format,
-          and at least one pre-generated key must be specified.
-        </para>
-        <para>
-          If you used the
-          <literal>boot.initrd.network.ssh.host*Key</literal> options,
-          you'll get an error explaining how to convert your host keys
-          and migrate to the new
-          <literal>boot.initrd.network.ssh.hostKeys</literal> option.
-          Otherwise, if you don't have any host keys set, you'll need to
-          generate some; see the <literal>hostKeys</literal> option
-          documentation for instructions.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Since this release there's an easy way to customize your PHP
-          install to get a much smaller base PHP with only wanted
-          extensions enabled. See the following snippet installing a
-          smaller PHP with the extensions <literal>imagick</literal>,
-          <literal>opcache</literal>, <literal>pdo</literal> and
-          <literal>pdo_mysql</literal> loaded:
-        </para>
-        <programlisting language="bash">
-{
-  environment.systemPackages = [
-    (pkgs.php.withExtensions
-      ({ all, ... }: with all; [
-        imagick
-        opcache
-        pdo
-        pdo_mysql
-      ])
-    )
-  ];
-}
-</programlisting>
-        <para>
-          The default <literal>php</literal> attribute hasn't lost any
-          extensions. The <literal>opcache</literal> extension has been
-          added. All upstream PHP extensions are available under
-          php.extensions.&lt;name?&gt;.
-        </para>
-        <para>
-          All PHP <literal>config</literal> flags have been removed for
-          the following reasons:
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The updated <literal>php</literal> attribute is now easily
-          customizable to your liking by using
-          <literal>php.withExtensions</literal> or
-          <literal>php.buildEnv</literal> instead of writing config
-          files or changing configure flags.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The remaining configuration flags can now be set directly on
-          the <literal>php</literal> attribute. For example, instead of
-        </para>
-        <programlisting language="bash">
-{
-  php.override {
-    config.php.embed = true;
-    config.php.apxs2 = false;
-  }
-}
-</programlisting>
-        <para>
-          you should now write
-        </para>
-        <programlisting language="bash">
-{
-  php.override {
-    embedSupport = true;
-    apxs2Support = false;
-  }
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The ACME module has been overhauled for simplicity and
-          maintainability. Cert generation now implicitly uses the
-          <literal>acme</literal> user, and the
-          <literal>security.acme.certs._name_.user</literal> option has
-          been removed. Instead, certificate access from other services
-          is now managed through group permissions. The module no longer
-          runs lego twice under certain conditions, and will correctly
-          renew certificates if their configuration is changed. Services
-          which reload nginx and httpd after certificate renewal are now
-          properly configured too so you no longer have to do this
-          manually if you are using HTTPS enabled virtual hosts. A
-          mechanism for regenerating certs on demand has also been added
-          and documented.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Gollum received a major update to version 5.x and you may have
-          to change some links in your wiki when migrating from gollum
-          4.x. More information can be found
-          <link xlink:href="https://github.com/gollum/gollum/wiki/5.0-release-notes#migrating-your-wiki">here</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Deluge 2.x was added and is used as default for new NixOS
-          installations where stateVersion is &gt;= 20.09. If you are
-          upgrading from a previous NixOS version, you can set
-          <literal>service.deluge.package = pkgs.deluge-2_x</literal> to
-          upgrade to Deluge 2.x and migrate the state to the new format.
-          Be aware that backwards state migrations are not supported by
-          Deluge.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Nginx web server now starting with additional
-          sandbox/hardening options. By default, write access to
-          <literal>/var/log/nginx</literal> and
-          <literal>/var/cache/nginx</literal> is allowed. To allow
-          writing to other folders, use
-          <literal>systemd.services.nginx.serviceConfig.ReadWritePaths</literal>
-        </para>
-        <programlisting language="bash">
-{
-  systemd.services.nginx.serviceConfig.ReadWritePaths = [ &quot;/var/www&quot; ];
-}
-</programlisting>
-        <para>
-          Nginx is also started with the systemd option
-          <literal>ProtectHome = mkDefault true;</literal> which forbids
-          it to read anything from <literal>/home</literal>,
-          <literal>/root</literal> and <literal>/run/user</literal> (see
-          <link xlink:href="https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome=">ProtectHome
-          docs</link> for details). If you require serving files from
-          home directories, you may choose to set e.g.
-        </para>
-        <programlisting language="bash">
-{
-  systemd.services.nginx.serviceConfig.ProtectHome = &quot;read-only&quot;;
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The NixOS options <literal>nesting.clone</literal> and
-          <literal>nesting.children</literal> have been deleted, and
-          replaced with named
-          <link xlink:href="options.html#opt-specialisation">specialisation</link>
-          configurations.
-        </para>
-        <para>
-          Replace a <literal>nesting.clone</literal> entry with:
-        </para>
-        <programlisting language="bash">
-{
-  specialisation.example-sub-configuration = {
-    configuration = {
-      ...
-    };
-};
-</programlisting>
-        <para>
-          Replace a <literal>nesting.children</literal> entry with:
-        </para>
-        <programlisting language="bash">
-{
-  specialisation.example-sub-configuration = {
-    inheritParentConfig = false;
-    configuration = {
-      ...
-    };
-};
-</programlisting>
-        <para>
-          To switch to a specialised configuration at runtime you need
-          to run:
-        </para>
-        <programlisting>
-$ sudo /run/current-system/specialisation/example-sub-configuration/bin/switch-to-configuration test
-</programlisting>
-        <para>
-          Before you would have used:
-        </para>
-        <programlisting>
-$ sudo /run/current-system/fine-tune/child-1/bin/switch-to-configuration test
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The Nginx log directory has been moved to
-          <literal>/var/log/nginx</literal>, the cache directory to
-          <literal>/var/cache/nginx</literal>. The option
-          <literal>services.nginx.stateDir</literal> has been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The httpd web server previously started its main process as
-          root privileged, then ran worker processes as a less
-          privileged identity user. This was changed to start all of
-          httpd as a less privileged user (defined by
-          <link xlink:href="options.html#opt-services.httpd.user">services.httpd.user</link>
-          and
-          <link xlink:href="options.html#opt-services.httpd.group">services.httpd.group</link>).
-          As a consequence, all files that are needed for httpd to run
-          (included configuration fragments, SSL certificates and keys,
-          etc.) must now be readable by this less privileged user/group.
-        </para>
-        <para>
-          The default value for
-          <link xlink:href="options.html#opt-services.httpd.mpm">services.httpd.mpm</link>
-          has been changed from <literal>prefork</literal> to
-          <literal>event</literal>. Along with this change the default
-          value for
-          <link xlink:href="options.html#opt-services.httpd.virtualHosts">services.httpd.virtualHosts.&lt;name&gt;.http2</link>
-          has been set to <literal>true</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>systemd-networkd</literal> option
-          <literal>systemd.network.networks.&lt;name&gt;.dhcp.CriticalConnection</literal>
-          has been removed following upstream systemd's deprecation of
-          the same. It is recommended to use
-          <literal>systemd.network.networks.&lt;name&gt;.networkConfig.KeepConfiguration</literal>
-          instead. See systemd.network 5 for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>systemd-networkd</literal> option
-          <literal>systemd.network.networks._name_.dhcpConfig</literal>
-          has been renamed to
-          <link xlink:href="options.html#opt-systemd.network.networks._name_.dhcpV4Config">systemd.network.networks.<emphasis>name</emphasis>.dhcpV4Config</link>
-          following upstream systemd's documentation change. See
-          systemd.network 5 for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          In the <literal>picom</literal> module, several options that
-          accepted floating point numbers encoded as strings (for
-          example
-          <link xlink:href="options.html#opt-services.picom.activeOpacity">services.picom.activeOpacity</link>)
-          have been changed to the (relatively) new native
-          <literal>float</literal> type. To migrate your configuration
-          simply remove the quotes around the numbers.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          When using <literal>buildBazelPackage</literal> from Nixpkgs,
-          <literal>flat</literal> hash mode is now used for dependencies
-          instead of <literal>recursive</literal>. This is to better
-          allow using hashed mirrors where needed. As a result, these
-          hashes will have changed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The syntax of the PostgreSQL configuration file is now checked
-          at build time. If your configuration includes a file
-          inaccessible inside the build sandbox, set
-          <literal>services.postgresql.checkConfig</literal> to
-          <literal>false</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The rkt module has been removed, it was archived by upstream.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="https://bazaar.canonical.com">Bazaar</link>
-          VCS is unmaintained and, as consequence of the Python 2 EOL,
-          the packages <literal>bazaar</literal> and
-          <literal>bazaarTools</literal> were removed. Breezy, the
-          backward compatible fork of Bazaar (see the
-          <link xlink:href="https://www.jelmer.uk/breezy-intro.html">announcement</link>),
-          was packaged as <literal>breezy</literal> and can be used
-          instead.
-        </para>
-        <para>
-          Regarding Nixpkgs, <literal>fetchbzr</literal>,
-          <literal>nix-prefetch-bzr</literal> and Bazaar support in
-          Hydra will continue to work through Breezy.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          In addition to the hostname, the fully qualified domain name
-          (FQDN), which consists of
-          <literal>${networking.hostName}</literal> and
-          <literal>${networking.domain}</literal> is now added to
-          <literal>/etc/hosts</literal>, to allow local FQDN resolution,
-          as used by the <literal>hostname --fqdn</literal> command and
-          other applications that try to determine the FQDN. These new
-          entries take precedence over entries from the DNS which could
-          cause regressions in some very specific setups. Additionally
-          the hostname is now resolved to <literal>127.0.0.2</literal>
-          instead of <literal>127.0.1.1</literal> to be consistent with
-          what <literal>nss-myhostname</literal> (from systemd) returns.
-          The old behaviour can e.g. be restored by using
-          <literal>networking.hosts = lib.mkForce { &quot;127.0.1.1&quot; = [ config.networking.hostName ]; };</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The hostname (<literal>networking.hostName</literal>) must now
-          be a valid DNS label (see RFC 1035, RFC 1123) and as such must
-          not contain the domain part. This means that the hostname must
-          start with a letter or digit, end with a letter or digit, and
-          have as interior characters only letters, digits, and hyphen.
-          The maximum length is 63 characters. Additionally it is
-          recommended to only use lower-case characters. If (e.g. for
-          legacy reasons) a FQDN is required as the Linux kernel network
-          node hostname (<literal>uname --nodename</literal>) the option
-          <literal>boot.kernel.sysctl.&quot;kernel.hostname&quot;</literal>
-          can be used as a workaround (but be aware of the 64 character
-          limit).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The GRUB specific option
-          <literal>boot.loader.grub.extraInitrd</literal> has been
-          replaced with the generic option
-          <literal>boot.initrd.secrets</literal>. This option creates a
-          secondary initrd from the specified files, rather than using a
-          manually created initrd file. Due to an existing bug with
-          <literal>boot.loader.grub.extraInitrd</literal>, it is not
-          possible to directly boot an older generation that used that
-          option. It is still possible to rollback to that generation if
-          the required initrd file has not been deleted.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="https://github.com/okTurtles/dnschain">DNSChain</link>
-          package and NixOS module have been removed from Nixpkgs as the
-          software is unmaintained and can't be built. For more
-          information see issue
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/89205">#89205</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          In the <literal>resilio</literal> module,
-          <link xlink:href="options.html#opt-services.resilio.httpListenAddr">services.resilio.httpListenAddr</link>
-          has been changed to listen to <literal>[::1]</literal> instead
-          of <literal>0.0.0.0</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>sslh</literal> has been updated to version
-          <literal>1.21</literal>. The <literal>ssl</literal> probe must
-          be renamed to <literal>tls</literal> in
-          <link xlink:href="options.html#opt-services.sslh.appendConfig">services.sslh.appendConfig</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Users of <link xlink:href="http://openafs.org">OpenAFS
-          1.6</link> must upgrade their services to OpenAFS 1.8! In this
-          release, the OpenAFS package version 1.6.24 is marked broken
-          but can be used during transition to OpenAFS 1.8.x. Use the
-          options
-          <literal>services.openafsClient.packages.module</literal>,
-          <literal>services.openafsClient.packages.programs</literal>
-          and <literal>services.openafsServer.package</literal> to
-          select a different OpenAFS package. OpenAFS 1.6 will be
-          removed in the next release. The package
-          <literal>openafs</literal> and the service options will then
-          silently point to the OpenAFS 1.8 release.
-        </para>
-        <para>
-          See also the OpenAFS
-          <link xlink:href="http://docs.openafs.org/AdminGuide/index.html">Administrator
-          Guide</link> for instructions. Beware of the following when
-          updating servers:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              The storage format of the server key has changed and the
-              key must be converted before running the new release.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              When updating multiple database servers, turn off the
-              database servers from the highest IP down to the lowest
-              with resting periods in between. Start up in reverse
-              order. Do not concurrently run database servers working
-              with different OpenAFS releases!
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Update servers first, then clients.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Radicale's default package has changed from 2.x to 3.x. An
-          upgrade checklist can be found
-          <link xlink:href="https://github.com/Kozea/Radicale/blob/3.0.x/NEWS.md#upgrade-checklist">here</link>.
-          You can use the newer version in the NixOS service by setting
-          the <literal>package</literal> to
-          <literal>radicale3</literal>, which is done automatically if
-          <literal>stateVersion</literal> is 20.09 or higher.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>udpt</literal> experienced a complete rewrite from
-          C++ to rust. The configuration format changed from ini to
-          toml. The new configuration documentation can be found at
-          <link xlink:href="https://naim94a.github.io/udpt/config.html">the
-          official website</link> and example configuration is packaged
-          in <literal>${udpt}/share/udpt/udpt.toml</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          We now have a unified
-          <link xlink:href="options.html#opt-services.xserver.displayManager.autoLogin">services.xserver.displayManager.autoLogin</link>
-          option interface to be used for every display-manager in
-          NixOS.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>bitcoind</literal> module has changed to
-          multi-instance, using submodules. Therefore, it is now
-          mandatory to name each instance. To use this new
-          multi-instance config with an existing bitcoind data directory
-          and user, you have to adjust the original config, e.g.:
-        </para>
-        <programlisting language="bash">
-{
-  services.bitcoind = {
-    enable = true;
-    extraConfig = &quot;...&quot;;
-    ...
-  };
-}
-</programlisting>
-        <para>
-          To something similar:
-        </para>
-        <programlisting language="bash">
-{
-  services.bitcoind.mainnet = {
-    enable = true;
-    dataDir = &quot;/var/lib/bitcoind&quot;;
-    user = &quot;bitcoin&quot;;
-    extraConfig = &quot;...&quot;;
-    ...
-  };
-}
-</programlisting>
-        <para>
-          The key settings are:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>dataDir</literal> - to continue using the same
-              data directory.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>user</literal> - to continue using the same user
-              so that bitcoind maintains access to its files.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Graylog introduced a change in the LDAP server certificate
-          validation behaviour for version 3.3.3 which might break
-          existing setups. When updating Graylog from a version before
-          3.3.3 make sure to check the Graylog
-          <link xlink:href="https://www.graylog.org/post/announcing-graylog-v3-3-3">release
-          info</link> for information on how to avoid the issue.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>dokuwiki</literal> module has changed to
-          multi-instance, using submodules. Therefore, it is now
-          mandatory to name each instance. Moreover, forcing SSL by
-          default has been dropped, so <literal>nginx.forceSSL</literal>
-          and <literal>nginx.enableACME</literal> are no longer set to
-          <literal>true</literal>. To continue using your service with
-          the original SSL settings, you have to adjust the original
-          config, e.g.:
-        </para>
-        <programlisting language="bash">
-{
-  services.dokuwiki = {
-    enable = true;
-    ...
-  };
-}
-</programlisting>
-        <para>
-          To something similar:
-        </para>
-        <programlisting language="bash">
-{
-  services.dokuwiki.&quot;mywiki&quot; = {
-    enable = true;
-    nginx = {
-      forceSSL = true;
-      enableACME = true;
-    };
-    ...
-  };
-}
-</programlisting>
-        <para>
-          The base package has also been upgraded to the 2020-07-29
-          &quot;Hogfather&quot; release. Plugins might be incompatible
-          or require upgrading.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.postgresql.dataDir">services.postgresql.dataDir</link>
-          option is now set to
-          <literal>&quot;/var/lib/postgresql/${cfg.package.psqlSchema}&quot;</literal>
-          regardless of your
-          <link xlink:href="options.html#opt-system.stateVersion">system.stateVersion</link>.
-          Users with an existing postgresql install that have a
-          <link xlink:href="options.html#opt-system.stateVersion">system.stateVersion</link>
-          of <literal>17.03</literal> or below should double check what
-          the value of their
-          <link xlink:href="options.html#opt-services.postgresql.dataDir">services.postgresql.dataDir</link>
-          option is (<literal>/var/db/postgresql</literal>) and then
-          explicitly set this value to maintain compatibility:
-        </para>
-        <programlisting language="bash">
-{
-  services.postgresql.dataDir = &quot;/var/db/postgresql&quot;;
-}
-</programlisting>
-        <para>
-          The postgresql module now expects there to be a database super
-          user account called <literal>postgres</literal> regardless of
-          your
-          <link xlink:href="options.html#opt-system.stateVersion">system.stateVersion</link>.
-          Users with an existing postgresql install that have a
-          <link xlink:href="options.html#opt-system.stateVersion">system.stateVersion</link>
-          of <literal>17.03</literal> or below should run the following
-          SQL statements as a database super admin user before
-          upgrading:
-        </para>
-        <programlisting language="SQL">
-CREATE ROLE postgres LOGIN SUPERUSER;
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The USBGuard module now removes options and instead hardcodes
-          values for <literal>IPCAccessControlFiles</literal>,
-          <literal>ruleFiles</literal>, and
-          <literal>auditFilePath</literal>. Audit logs can be found in
-          the journal.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The NixOS module system now evaluates option definitions more
-          strictly, allowing it to detect a larger set of problems. As a
-          result, what previously evaluated may not do so anymore. See
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/82743#issuecomment-674520472">the
-          PR that changed this</link> for more info.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          For NixOS configuration options, the type
-          <literal>loaOf</literal>, after its initial deprecation in
-          release 20.03, has been removed. In NixOS and Nixpkgs options
-          using this type have been converted to
-          <literal>attrsOf</literal>. For more information on this
-          change have look at these links:
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/1800">issue
-          #1800</link>,
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/63103">PR
-          #63103</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>config.systemd.services.${name}.path</literal> now
-          returns a list of paths instead of a colon-separated string.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Caddy module now uses Caddy v2 by default. Caddy v1 can still
-          be used by setting
-          <link xlink:href="options.html#opt-services.caddy.package">services.caddy.package</link>
-          to <literal>pkgs.caddy1</literal>.
-        </para>
-        <para>
-          New option
-          <link xlink:href="options.html#opt-services.caddy.adapter">services.caddy.adapter</link>
-          has been added.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.jellyfin.enable">jellyfin</link>
-          module will use and stay on the Jellyfin version
-          <literal>10.5.5</literal> if <literal>stateVersion</literal>
-          is lower than <literal>20.09</literal>. This is because
-          significant changes were made to the database schema, and it
-          is highly recommended to backup your instance before
-          upgrading. After making your backup, you can upgrade to the
-          latest version either by setting your
-          <literal>stateVersion</literal> to <literal>20.09</literal> or
-          higher, or set the
-          <literal>services.jellyfin.package</literal> to
-          <literal>pkgs.jellyfin</literal>. If you do not wish to
-          upgrade Jellyfin, but want to change your
-          <literal>stateVersion</literal>, you can set the value of
-          <literal>services.jellyfin.package</literal> to
-          <literal>pkgs.jellyfin_10_5</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>security.rngd</literal> service is now disabled
-          by default. This choice was made because there's krngd in the
-          linux kernel space making it (for most usecases) functionally
-          redundent.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>hardware.nvidia.optimus_prime.enable</literal>
-          service has been renamed to
-          <literal>hardware.nvidia.prime.sync.enable</literal> and has
-          many new enhancements. Related nvidia prime settings may have
-          also changed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The package nextcloud17 has been removed and nextcloud18 was
-          marked as insecure since both of them will
-          <link xlink:href="https://docs.nextcloud.com/server/19/admin_manual/release_schedule.html">
-          will be EOL (end of life) within the lifetime of 20.09</link>.
-        </para>
-        <para>
-          It's necessary to upgrade to nextcloud19:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              From nextcloud17, you have to upgrade to nextcloud18 first
-              as Nextcloud doesn't allow going multiple major revisions
-              forward in a single upgrade. This is possible by setting
-              <link xlink:href="options.html#opt-services.nextcloud.package">services.nextcloud.package</link>
-              to nextcloud18.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              From nextcloud18, it's possible to directly upgrade to
-              nextcloud19 by setting
-              <link xlink:href="options.html#opt-services.nextcloud.package">services.nextcloud.package</link>
-              to nextcloud19.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The GNOME desktop manager no longer default installs
-          gnome3.epiphany. It was chosen to do this as it has a
-          usability breaking issue (see issue
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/98819">#98819</link>)
-          that makes it unsuitable to be a default app.
-        </para>
-        <note>
-          <para>
-            Issue
-            <link xlink:href="https://github.com/NixOS/nixpkgs/issues/98819">#98819</link>
-            is now fixed and gnome3.epiphany is once again installed by
-            default.
-          </para>
-        </note>
-      </listitem>
-      <listitem>
-        <para>
-          If you want to manage the configuration of wpa_supplicant
-          outside of NixOS you must ensure that none of
-          <link xlink:href="options.html#opt-networking.wireless.networks">networking.wireless.networks</link>,
-          <link xlink:href="options.html#opt-networking.wireless.extraConfig">networking.wireless.extraConfig</link>
-          or
-          <link xlink:href="options.html#opt-networking.wireless.userControlled.enable">networking.wireless.userControlled.enable</link>
-          is being used or <literal>true</literal>. Using any of those
-          options will cause wpa_supplicant to be started with a NixOS
-          generated configuration file instead of your own.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-20.09-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          SD images are now compressed by default using
-          <literal>zstd</literal>. The compression for ISO images has
-          also been changed to <literal>zstd</literal>, but ISO images
-          are still not compressed by default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.journald.rateLimitBurst</literal> was
-          updated from <literal>1000</literal> to
-          <literal>10000</literal> to follow the new upstream systemd
-          default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The notmuch package moves its emacs-related binaries and emacs
-          lisp files to a separate output. They're not part of the
-          default <literal>out</literal> output anymore - if you relied
-          on the <literal>notmuch-emacs-mua</literal> binary or the
-          emacs lisp files, access them via the
-          <literal>notmuch.emacs</literal> output.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Device tree overlay support was improved in
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/79370">#79370</link>
-          and now uses
-          <link xlink:href="options.html#opt-hardware.deviceTree.kernelPackage">hardware.deviceTree.kernelPackage</link>
-          instead of <literal>hardware.deviceTree.base</literal>.
-          <link xlink:href="options.html#opt-hardware.deviceTree.overlays">hardware.deviceTree.overlays</link>
-          configuration was extended to support <literal>.dts</literal>
-          files with symbols. Device trees can now be filtered by
-          setting
-          <link xlink:href="options.html#opt-hardware.deviceTree.filter">hardware.deviceTree.filter</link>
-          option.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default output of <literal>buildGoPackage</literal> is now
-          <literal>$out</literal> instead of <literal>$bin</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>buildGoModule</literal> <literal>doCheck</literal>
-          now defaults to <literal>true</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Packages built using <literal>buildRustPackage</literal> now
-          use <literal>release</literal> mode for the
-          <literal>checkPhase</literal> by default.
-        </para>
-        <para>
-          Please note that Rust packages utilizing a custom
-          build/install procedure (e.g. by using a
-          <literal>Makefile</literal>) or test suites that rely on the
-          structure of the <literal>target/</literal> directory may
-          break due to those assumptions. For further information,
-          please read the Rust section in the Nixpkgs manual.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The cc- and binutils-wrapper's &quot;infix salt&quot; and
-          <literal>_BUILD_</literal> and <literal>_TARGET_</literal>
-          user infixes have been replaced with with a &quot;suffix
-          salt&quot; and suffixes and <literal>_FOR_BUILD</literal> and
-          <literal>_FOR_TARGET</literal>. This matches the autotools
-          convention for env vars which standard for these things,
-          making interfacing with other tools easier.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Additional Git documentation (HTML and text files) is now
-          available via the <literal>git-doc</literal> package.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Default algorithm for ZRAM swap was changed to
-          <literal>zstd</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The installer now enables sshd by default. This improves
-          installation on headless machines especially ARM
-          single-board-computer. To login through ssh, either a password
-          or an ssh key must be set for the root user or the nixos user.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The scripted networking system now uses
-          <literal>.link</literal> files in
-          <literal>/etc/systemd/network</literal> to configure mac
-          address and link MTU, instead of the sometimes buggy
-          <literal>network-link-*</literal> units, which have been
-          removed. Bringing the interface up has been moved to the
-          beginning of the <literal>network-addresses-*</literal> unit.
-          Note this doesn't require <literal>systemd-networkd</literal>
-          - it's udev that parses <literal>.link</literal> files. Extra
-          care needs to be taken in the presence of
-          <link xlink:href="https://wiki.debian.org/NetworkInterfaceNames#THE_.22PERSISTENT_NAMES.22_SCHEME">legacy
-          udev rules</link> to rename interfaces, as MAC Address and MTU
-          defined in these options can only match on the original link
-          name. In such cases, you most likely want to create a
-          <literal>10-*.link</literal> file through
-          <link xlink:href="options.html#opt-systemd.network.links">systemd.network.links</link>
-          and set both name and MAC Address / MTU there.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Grafana received a major update to version 7.x. A plugin is
-          now needed for image rendering support, and plugins must now
-          be signed by default. More information can be found
-          <link xlink:href="https://grafana.com/docs/grafana/latest/installation/upgrading/#upgrading-to-v7-0">in
-          the Grafana documentation</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>hardware.u2f</literal> module, which was
-          installing udev rules was removed, as udev gained native
-          support to handle FIDO security tokens.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.transmission</literal> module was
-          enhanced with the new options:
-          <link xlink:href="options.html#opt-services.transmission.credentialsFile">services.transmission.credentialsFile</link>,
-          <link xlink:href="options.html#opt-services.transmission.openFirewall">services.transmission.openFirewall</link>,
-          and
-          <link xlink:href="options.html#opt-services.transmission.performanceNetParameters">services.transmission.performanceNetParameters</link>.
-        </para>
-        <para>
-          <literal>transmission-daemon</literal> is now started with
-          additional systemd sandbox/hardening options for better
-          security. Please
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues">report</link>
-          any use case where this is not working well. In particular,
-          the <literal>RootDirectory</literal> option newly set forbids
-          uploading or downloading a torrent outside of the default
-          directory configured at
-          <link xlink:href="options.html#opt-services.transmission.settings">settings.download-dir</link>.
-          If you really need Transmission to access other directories,
-          you must include those directories into the
-          <literal>BindPaths</literal> of the service:
-        </para>
-        <programlisting language="bash">
-{
-  systemd.services.transmission.serviceConfig.BindPaths = [ &quot;/path/to/alternative/download-dir&quot; ];
-}
-</programlisting>
-        <para>
-          Also, connection to the RPC (Remote Procedure Call) of
-          <literal>transmission-daemon</literal> is now only available
-          on the local network interface by default. Use:
-        </para>
-        <programlisting language="bash">
-{
-  services.transmission.settings.rpc-bind-address = &quot;0.0.0.0&quot;;
-}
-</programlisting>
-        <para>
-          to get the previous behavior of listening on all network
-          interfaces.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          With this release <literal>systemd-networkd</literal> (when
-          enabled through
-          <link xlink:href="options.html#opt-networking.useNetworkd">networking.useNetworkd</link>)
-          has it's netlink socket created through a
-          <literal>systemd.socket</literal> unit. This gives us control
-          over socket buffer sizes and other parameters. For larger
-          setups where networkd has to create a lot of (virtual) devices
-          the default buffer size (currently 128MB) is not enough.
-        </para>
-        <para>
-          On a machine with &gt;100 virtual interfaces (e.g., wireguard
-          tunnels, VLANs, …), that all have to be brought up during
-          system startup, the receive buffer size will spike for a brief
-          period. Eventually some of the message will be dropped since
-          there is not enough (permitted) buffer space available.
-        </para>
-        <para>
-          By having <literal>systemd-networkd</literal> start with a
-          netlink socket created by <literal>systemd</literal> we can
-          configure the <literal>ReceiveBufferSize=</literal> parameter
-          in the socket options (i.e.
-          <literal>systemd.sockets.systemd-networkd.socketOptions.ReceiveBufferSize</literal>)
-          without recompiling <literal>systemd-networkd</literal>.
-        </para>
-        <para>
-          Since the actual memory requirements depend on hardware,
-          timing, exact configurations etc. it isn't currently possible
-          to infer a good default from within the NixOS module system.
-          Administrators are advised to monitor the logs of
-          <literal>systemd-networkd</literal> for
-          <literal>rtnl: kernel receive buffer overrun</literal> spam
-          and increase the memory limit as they see fit.
-        </para>
-        <para>
-          Note: Increasing the <literal>ReceiveBufferSize=</literal>
-          doesn't allocate any memory. It just increases the upper bound
-          on the kernel side. The memory allocation depends on the
-          amount of messages that are queued on the kernel side of the
-          netlink socket.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Specifying
-          <link xlink:href="options.html#opt-services.dovecot2.mailboxes">mailboxes</link>
-          in the dovecot2 module as a list is deprecated and will break
-          eval in 21.05. Instead, an attribute-set should be specified
-          where the <literal>name</literal> should be the key of the
-          attribute.
-        </para>
-        <para>
-          This means that a configuration like this
-        </para>
-        <programlisting language="bash">
-{
-  services.dovecot2.mailboxes = [
-    { name = &quot;Junk&quot;;
-      auto = &quot;create&quot;;
-    }
-  ];
-}
-</programlisting>
-        <para>
-          should now look like this:
-        </para>
-        <programlisting language="bash">
-{
-  services.dovecot2.mailboxes = {
-    Junk.auto = &quot;create&quot;;
-  };
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          netbeans was upgraded to 12.0 and now defaults to OpenJDK 11.
-          This might cause problems if your projects depend on packages
-          that were removed in Java 11.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          nextcloud has been updated to
-          <link xlink:href="https://nextcloud.com/blog/nextcloud-hub-brings-productivity-to-home-office/">v19</link>.
-        </para>
-        <para>
-          If you have an existing installation, please make sure that
-          you're on nextcloud18 before upgrading to nextcloud19 since
-          Nextcloud doesn't support upgrades across multiple major
-          versions.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>nixos-run-vms</literal> script now deletes the
-          previous run machines states on test startup. You can use the
-          <literal>--keep-vm-state</literal> flag to match the previous
-          behaviour and keep the same VM state between different test
-          runs.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-nix.buildMachines">nix.buildMachines</link>
-          option is now type-checked. There are no functional changes,
-          however this may require updating some configurations to use
-          correct types for all attributes.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>fontconfig</literal> module stopped generating
-          config and cache files for fontconfig 2.10.x, the
-          <literal>/etc/fonts/fonts.conf</literal> now belongs to the
-          latest fontconfig, just like on other Linux distributions, and
-          we will
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/95358">no
-          longer</link> be versioning the config directories.
-        </para>
-        <para>
-          Fontconfig 2.10.x was removed from Nixpkgs since it hasn’t
-          been used in any Nixpkgs package for years now.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Nginx module
-          <literal>nginxModules.fastcgi-cache-purge</literal> renamed to
-          official name <literal>nginxModules.cache-purge</literal>.
-          Nginx module <literal>nginxModules.ngx_aws_auth</literal>
-          renamed to official name
-          <literal>nginxModules.aws-auth</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option <literal>defaultPackages</literal> was added. It
-          installs the packages perl, rsync and strace for now. They
-          were added unconditionally to
-          <literal>systemPackages</literal> before, but are not strictly
-          necessary for a minimal NixOS install. You can set it to an
-          empty list to have a more minimal system. Be aware that some
-          functionality might still have an impure dependency on those
-          packages, so things might break.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>undervolt</literal> option no longer needs to
-          apply its settings every 30s. If they still become undone,
-          open an issue and restore the previous behaviour using
-          <literal>undervolt.useTimer</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Agda has been heavily reworked.
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>agda.mkDerivation</literal> has been heavily
-              changed and is now located at agdaPackages.mkDerivation.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              New top-level packages agda and
-              <literal>agda.withPackages</literal> have been added, the
-              second of which sets up agda with access to chosen
-              libraries.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              All agda libraries now live under
-              <literal>agdaPackages</literal>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Many broken libraries have been removed.
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          See the
-          <link xlink:href="https://nixos.org/nixpkgs/manual/#agda">new
-          documentation</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>deepin</literal> package set has been removed
-          from nixpkgs. It was a work in progress to package the
-          <link xlink:href="https://www.deepin.org/en/dde/">Deepin
-          Desktop Environment (DDE)</link>, including libraries, tools
-          and applications, and it was still missing a service to launch
-          the desktop environment. It has shown to no longer be a
-          feasible goal due to reasons discussed in
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/94870">issue
-          #94870</link>. The package
-          <literal>netease-cloud-music</literal> has also been removed,
-          as it depends on libraries from deepin.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>opendkim</literal> module now uses systemd
-          sandboxing features to limit the exposure of the system
-          towards the opendkim service.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Kubernetes has been upgraded to 1.19.1, which also means that
-          the golang version to build it has been bumped to 1.15. This
-          may have consequences for your existing clusters and their
-          certificates. Please consider
-          <link xlink:href="https://relnotes.k8s.io/?markdown=93264">
-          the release notes for Kubernetes 1.19 carefully </link> before
-          upgrading.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          For AMD GPUs, Vulkan can now be used by adding
-          <literal>amdvlk</literal> to
-          <literal>hardware.opengl.extraPackages</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Similarly, still for AMD GPUs, the ROCm OpenCL stack can now
-          be used by adding <literal>rocm-opencl-icd</literal> to
-          <literal>hardware.opengl.extraPackages</literal>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-20.09-contributions">
-    <title>Contributions</title>
-    <para>
-      I, Jonathan Ringer, would like to thank the following individuals
-      for their work on nixpkgs. This release could not be done without
-      the hard work of the NixOS community. There were 31282
-      contributions across 1313 contributors.
-    </para>
-    <orderedlist numeration="arabic">
-      <listitem>
-        <para>
-          2288 Mario Rodas
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          1837 Frederik Rietdijk
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          946 Jörg Thalheim
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          925 Maximilian Bosch
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          687 Jonathan Ringer
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          651 Jan Tojnar
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          622 Daniël de Kok
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          605 WORLDofPEACE
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          597 Florian Klink
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          528 José Romildo Malaquias
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          281 volth
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          101 Robert Scott
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          86 Tim Steinbach
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          76 WORLDofPEACE
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          49 Maximilian Bosch
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          42 Thomas Tuegel
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          37 Doron Behar
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          36 Vladimír Čunát
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          27 Jonathan Ringer
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          27 Maciej Krüger
-        </para>
-      </listitem>
-    </orderedlist>
-    <para>
-      I, Jonathan Ringer, would also like to personally thank
-      @WORLDofPEACE for their help in mentoring me on the release
-      process. Special thanks also goes to Thomas Tuegel for helping
-      immensely with stabilizing Qt, KDE, and Plasma5; I would also like
-      to thank Robert Scott for his numerous fixes and pull request
-      reviews.
-    </para>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2105.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2105.section.xml
deleted file mode 100644
index 3477f29f428..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-2105.section.xml
+++ /dev/null
@@ -1,1567 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-21.05">
-  <title>Release 21.05 (<quote>Okapi</quote>, 2021.05/31)</title>
-  <para>
-    Support is planned until the end of December 2021, handing over to
-    21.11.
-  </para>
-  <section xml:id="sec-release-21.05-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      has the following highlights:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Core version changes:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              gcc: 9.3.0 -&gt; 10.3.0
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              glibc: 2.30 -&gt; 2.32
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              default linux: 5.4 -&gt; 5.10, all supported kernels
-              available
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              mesa: 20.1.7 -&gt; 21.0.1
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Desktop Environments:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              GNOME: 3.36 -&gt; 40, see its
-              <link xlink:href="https://help.gnome.org/misc/release-notes/40.0/">release
-              notes</link>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Plasma5: 5.18.5 -&gt; 5.21.3
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              kdeApplications: 20.08.1 -&gt; 20.12.3
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              cinnamon: 4.6 -&gt; 4.8.1
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Programming Languages and Frameworks:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              Python optimizations were disabled again. Builds with
-              optimizations enabled are not reproducible. Optimizations
-              can now be enabled with an option.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The linux_latest kernel was updated to the 5.13 series. It
-          currently is not officially supported for use with the zfs
-          filesystem. If you use zfs, you should use a different kernel
-          version (either the LTS kernel, or track a specific one).
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-21.05-new-services">
-    <title>New Services</title>
-    <para>
-      The following new services were added since the last release:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.gnuradio.org/">GNURadio</link>
-          3.8 and 3.9 were
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/82263">finally</link>
-          packaged, along with a rewrite to the Nix expressions,
-          allowing users to override the features upstream supports
-          selecting to compile or not to. Additionally, the attribute
-          <literal>gnuradio</literal> (3.9),
-          <literal>gnuradio3_8</literal> and
-          <literal>gnuradio3_7</literal> now point to an externally
-          wrapped by default derivations, that allow you to also add
-          `extraPythonPackages` to the Python interpreter used by
-          GNURadio. Missing environmental variables needed for
-          operational GUI were also added
-          (<link xlink:href="https://github.com/NixOS/nixpkgs/issues/75478">#75478</link>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.keycloak.org/">Keycloak</link>,
-          an open source identity and access management server with
-          support for
-          <link xlink:href="https://openid.net/connect/">OpenID
-          Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH
-          2.0</link> and
-          <link xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
-          2.0</link>.
-        </para>
-        <para>
-          See the <link linkend="module-services-keycloak">Keycloak
-          section of the NixOS manual</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="options.html#opt-services.samba-wsdd.enable">services.samba-wsdd.enable</link>
-          Web Services Dynamic Discovery host daemon
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.discourse.org/">Discourse</link>,
-          a modern and open source discussion platform.
-        </para>
-        <para>
-          See the <link linkend="module-services-discourse">Discourse
-          section of the NixOS manual</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="options.html#opt-services.nebula.networks">services.nebula.networks</link>
-          <link xlink:href="https://github.com/slackhq/nebula">Nebula
-          VPN</link>
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-21.05-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <para>
-      When upgrading from a previous release, please be aware of the
-      following incompatible changes:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          GNOME desktop environment was upgraded to 40, see the release
-          notes for
-          <link xlink:href="https://help.gnome.org/misc/release-notes/40.0/">40.0</link>
-          and
-          <link xlink:href="https://help.gnome.org/misc/release-notes/3.38/">3.38</link>.
-          The <literal>gnome3</literal> attribute set has been renamed
-          to <literal>gnome</literal> and so have been the NixOS
-          options.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          If you are using <literal>services.udev.extraRules</literal>
-          to assign custom names to network interfaces, this may stop
-          working due to a change in the initialisation of dhcpcd and
-          systemd networkd. To avoid this, either move them to
-          <literal>services.udev.initrdRules</literal> or see the new
-          <link linkend="sec-custom-ifnames">Assigning custom
-          names</link> section of the NixOS manual for an example using
-          networkd links.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>security.hideProcessInformation</literal> module
-          has been removed. It was broken since the switch to
-          cgroups-v2.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>linuxPackages.ati_drivers_x11</literal> kernel
-          modules have been removed. The drivers only supported kernels
-          prior to 4.2, and thus have become obsolete.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>systemConfig</literal> kernel parameter is no
-          longer added to boot loader entries. It has been unused since
-          September 2010, but if do have a system generation from that
-          era, you will now be unable to boot into them.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>systemd-journal2gelf</literal> no longer parses json
-          and expects the receiving system to handle it. How to achieve
-          this with Graylog is described in this
-          <link xlink:href="https://github.com/parse-nl/SystemdJournal2Gelf/issues/10">GitHub
-          issue</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          If the <literal>services.dbus</literal> module is enabled,
-          then the user D-Bus session is now always socket activated.
-          The associated options
-          <literal>services.dbus.socketActivated</literal> and
-          <literal>services.xserver.startDbusSession</literal> have
-          therefore been removed and you will receive a warning if they
-          are present in your configuration. This change makes the user
-          D-Bus session available also for non-graphical logins.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>networking.wireless.iwd</literal> module now
-          installs the upstream-provided 80-iwd.link file, which sets
-          the NamePolicy= for all wlan devices to &quot;keep
-          kernel&quot;, to avoid race conditions between iwd and
-          networkd. If you don't want this, you can set
-          <literal>systemd.network.links.&quot;80-iwd&quot; = lib.mkForce {}</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>rubyMinimal</literal> was removed due to being unused
-          and unusable. The default ruby interpreter includes JIT
-          support, which makes it reference it's compiler. Since JIT
-          support is probably needed by some Gems, it was decided to
-          enable this feature with all cc references by default, and
-          allow to build a Ruby derivation without references to cc, by
-          setting <literal>jitSupport = false;</literal> in an overlay.
-          See
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/90151">#90151</link>
-          for more info.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Setting
-          <literal>services.openssh.authorizedKeysFiles</literal> now
-          also affects which keys
-          <literal>security.pam.enableSSHAgentAuth</literal> will use.
-          WARNING: If you are using these options in combination do make
-          sure that any key paths you use are present in
-          <literal>services.openssh.authorizedKeysFiles</literal>!
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option <literal>fonts.enableFontDir</literal> has been
-          renamed to
-          <link xlink:href="options.html#opt-fonts.fontDir.enable">fonts.fontDir.enable</link>.
-          The path of font directory has also been changed to
-          <literal>/run/current-system/sw/share/X11/fonts</literal>, for
-          consistency with other X11 resources.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          A number of options have been renamed in the kicad interface.
-          <literal>oceSupport</literal> has been renamed to
-          <literal>withOCE</literal>, <literal>withOCCT</literal> has
-          been renamed to <literal>withOCC</literal>,
-          <literal>ngspiceSupport</literal> has been renamed to
-          <literal>withNgspice</literal>, and
-          <literal>scriptingSupport</literal> has been renamed to
-          <literal>withScripting</literal>. Additionally,
-          <literal>kicad/base.nix</literal> no longer provides default
-          argument values since these are provided by
-          <literal>kicad/default.nix</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The socket for the <literal>pdns-recursor</literal> module was
-          moved from <literal>/var/lib/pdns-recursor</literal> to
-          <literal>/run/pdns-recursor</literal> to match upstream.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Paperwork was updated to version 2. The on-disk format
-          slightly changed, and it is not possible to downgrade from
-          Paperwork 2 back to Paperwork 1.3. Back your documents up
-          before upgrading. See
-          <link xlink:href="https://forum.openpaper.work/t/paperwork-2-0/112/5">this
-          thread</link> for more details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PowerDNS has been updated from <literal>4.2.x</literal> to
-          <literal>4.3.x</literal>. Please be sure to review the
-          <link xlink:href="https://doc.powerdns.com/authoritative/upgrading.html#x-to-4-3-0">Upgrade
-          Notes</link> provided by upstream before upgrading. Worth
-          specifically noting is that the service now runs entirely as a
-          dedicated <literal>pdns</literal> user, instead of starting as
-          <literal>root</literal> and dropping privileges, as well as
-          the default <literal>socket-dir</literal> location changing
-          from <literal>/var/lib/powerdns</literal> to
-          <literal>/run/pdns</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>mediatomb</literal> service is now using by
-          default the new and maintained fork <literal>gerbera</literal>
-          package instead of the unmaintained
-          <literal>mediatomb</literal> package. If you want to keep the
-          old behavior, you must declare it with:
-        </para>
-        <programlisting language="bash">
-{
-  services.mediatomb.package = pkgs.mediatomb;
-}
-</programlisting>
-        <para>
-          One new option <literal>openFirewall</literal> has been
-          introduced which defaults to false. If you relied on the
-          service declaration to add the firewall rules itself before,
-          you should now declare it with:
-        </para>
-        <programlisting language="bash">
-{
-  services.mediatomb.openFirewall = true;
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          xfsprogs was update from 4.19 to 5.11. It now enables reflink
-          support by default on filesystem creation. Support for
-          reflinks was added with an experimental status to kernel 4.9
-          and deemed stable in kernel 4.16. If you want to be able to
-          mount XFS filesystems created with this release of xfsprogs on
-          kernel releases older than those, you need to format them with
-          <literal>mkfs.xfs -m reflink=0</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The uWSGI server is now built with POSIX capabilities. As a
-          consequence, root is no longer required in emperor mode and
-          the service defaults to running as the unprivileged
-          <literal>uwsgi</literal> user. Any additional capability can
-          be added via the new option
-          <link xlink:href="options.html#opt-services.uwsgi.capabilities">services.uwsgi.capabilities</link>.
-          The previous behaviour can be restored by setting:
-        </para>
-        <programlisting language="bash">
-{
-  services.uwsgi.user = &quot;root&quot;;
-  services.uwsgi.group = &quot;root&quot;;
-  services.uwsgi.instance =
-    {
-      uid = &quot;uwsgi&quot;;
-      gid = &quot;uwsgi&quot;;
-    };
-}
-</programlisting>
-        <para>
-          Another incompatibility from the previous release is that
-          vassals running under a different user or group need to use
-          <literal>immediate-{uid,gid}</literal> instead of the usual
-          <literal>uid,gid</literal> options.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          btc1 has been abandoned upstream, and removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          cpp_ethereum (aleth) has been abandoned upstream, and removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          riak-cs package removed along with
-          <literal>services.riak-cs</literal> module.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          stanchion package removed along with
-          <literal>services.stanchion</literal> module.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          mutt has been updated to a new major version (2.x), which
-          comes with some backward incompatible changes that are
-          described in the
-          <link xlink:href="http://www.mutt.org/relnotes/2.0/">release
-          notes for Mutt 2.0</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>vim</literal> and <literal>neovim</literal> switched
-          to Python 3, dropping all Python 2 support.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="options.html#opt-networking.wireguard.interfaces">networking.wireguard.interfaces.&lt;name&gt;.generatePrivateKeyFile</link>,
-          which is off by default, had a <literal>chmod</literal> race
-          condition fixed. As an aside, the parent directory's
-          permissions were widened, and the key files were made
-          owner-writable. This only affects newly created keys. However,
-          if the exact permissions are important for your setup, read
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/121294">#121294</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="options.html#opt-boot.zfs.forceImportAll">boot.zfs.forceImportAll</link>
-          previously did nothing, but has been fixed. However its
-          default has been changed to <literal>false</literal> to
-          preserve the existing default behaviour. If you have this
-          explicitly set to <literal>true</literal>, please note that
-          your non-root pools will now be forcibly imported.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          openafs now points to openafs_1_8, which is the new stable
-          release. OpenAFS 1.6 was removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The WireGuard module gained a new option
-          <literal>networking.wireguard.interfaces.&lt;name&gt;.peers.*.dynamicEndpointRefreshSeconds</literal>
-          that implements refreshing the IP of DNS-based endpoints
-          periodically (which WireGuard itself
-          <link xlink:href="https://lists.zx2c4.com/pipermail/wireguard/2017-November/002028.html">cannot
-          do</link>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          MariaDB has been updated to 10.5. Before you upgrade, it would
-          be best to take a backup of your database and read
-          <link xlink:href="https://mariadb.com/kb/en/upgrading-from-mariadb-104-to-mariadb-105/#incompatible-changes-between-104-and-105">
-          Incompatible Changes Between 10.4 and 10.5</link>. After the
-          upgrade you will need to run <literal>mysql_upgrade</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The TokuDB storage engine dropped in mariadb 10.5 and removed
-          in mariadb 10.6. It is recommended to switch to RocksDB. See
-          also
-          <link xlink:href="https://mariadb.com/kb/en/tokudb/">TokuDB</link>
-          and
-          <link xlink:href="https://jira.mariadb.org/browse/MDEV-19780">MDEV-19780:
-          Remove the TokuDB storage engine</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>openldap</literal> module now has support for
-          OLC-style configuration, users of the
-          <literal>configDir</literal> option may wish to migrate. If
-          you continue to use <literal>configDir</literal>, ensure that
-          <literal>olcPidFile</literal> is set to
-          <literal>/run/slapd/slapd.pid</literal>.
-        </para>
-        <para>
-          As a result, <literal>extraConfig</literal> and
-          <literal>extraDatabaseConfig</literal> are removed. To help
-          with migration, you can convert your
-          <literal>slapd.conf</literal> file to OLC configuration with
-          the following script (find the location of this configuration
-          file by running <literal>systemctl status openldap</literal>,
-          it is the <literal>-f</literal> option.
-        </para>
-        <programlisting>
-$ TMPDIR=$(mktemp -d)
-$ slaptest -f /path/to/slapd.conf -F $TMPDIR
-$ slapcat -F $TMPDIR -n0 -H 'ldap:///???(!(objectClass=olcSchemaConfig))'
-</programlisting>
-        <para>
-          This will dump your current configuration in LDIF format,
-          which should be straightforward to convert into Nix settings.
-          This does not show your schema configuration, as this is
-          unnecessarily verbose for users of the default schemas and
-          <literal>slaptest</literal> is buggy with schemas directly in
-          the config file.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Amazon EC2 and OpenStack Compute (nova) images now re-fetch
-          instance meta data and user data from the instance metadata
-          service (IMDS) on each boot. For example: stopping an EC2
-          instance, changing its user data, and restarting the instance
-          will now cause it to fetch and apply the new user data.
-        </para>
-        <warning>
-          <para>
-            Specifically, <literal>/etc/ec2-metadata</literal> is
-            re-populated on each boot. Some NixOS scripts that read from
-            this directory are guarded to only run if the files they
-            want to manipulate do not already exist, and so will not
-            re-apply their changes if the IMDS response changes.
-            Examples: <literal>root</literal>'s SSH key is only added if
-            <literal>/root/.ssh/authorized_keys</literal> does not
-            exist, and SSH host keys are only set from user data if they
-            do not exist in <literal>/etc/ssh</literal>.
-          </para>
-        </warning>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>rspamd</literal> services is now sandboxed. It is
-          run as a dynamic user instead of root, so secrets and other
-          files may have to be moved or their permissions may have to be
-          fixed. The sockets are now located in
-          <literal>/run/rspamd</literal> instead of
-          <literal>/run</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Enabling the Tor client no longer silently also enables and
-          configures Privoxy, and the
-          <literal>services.tor.client.privoxy.enable</literal> option
-          has been removed. To enable Privoxy, and to configure it to
-          use Tor's faster port, use the following configuration:
-        </para>
-        <programlisting language="bash">
-{
-  opt-services.privoxy.enable = true;
-  opt-services.privoxy.enableTor = true;
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.tor</literal> module has a new
-          exhaustively typed
-          <link xlink:href="options.html#opt-services.tor.settings">services.tor.settings</link>
-          option following RFC 0042; backward compatibility with old
-          options has been preserved when aliasing was possible. The
-          corresponding systemd service has been hardened, but there is
-          a chance that the service still requires more permissions, so
-          please report any related trouble on the bugtracker. Onion
-          services v3 are now supported in
-          <link xlink:href="options.html#opt-services.tor.relay.onionServices">services.tor.relay.onionServices</link>.
-          A new
-          <link xlink:href="options.html#opt-services.tor.openFirewall">services.tor.openFirewall</link>
-          option as been introduced for allowing connections on all the
-          TCP ports configured.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The options
-          <literal>services.slurm.dbdserver.storagePass</literal> and
-          <literal>services.slurm.dbdserver.configFile</literal> have
-          been removed. Use
-          <literal>services.slurm.dbdserver.storagePassFile</literal>
-          instead to provide the database password. Extra config options
-          can be given via the option
-          <literal>services.slurm.dbdserver.extraConfig</literal>. The
-          actual configuration file is created on the fly on startup of
-          the service. This avoids that the password gets exposed in the
-          nix store.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>wafHook</literal> hook does not wrap Python
-          anymore. Packages depending on <literal>wafHook</literal> need
-          to include any Python into their
-          <literal>nativeBuildInputs</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Starting with version 1.7.0, the project formerly named
-          <literal>CodiMD</literal> is now named
-          <literal>HedgeDoc</literal>. New installations will no longer
-          use the old name for users, state directories and such, this
-          needs to be considered when moving state to a more recent
-          NixOS installation. Based on
-          <link xlink:href="options.html#opt-system.stateVersion">system.stateVersion</link>,
-          existing installations will continue to work.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The fish-foreign-env package has been replaced with
-          fishPlugins.foreign-env, in which the fish functions have been
-          relocated to the <literal>vendor_functions.d</literal>
-          directory to be loaded automatically.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The prometheus json exporter is now managed by the prometheus
-          community. Together with additional features some backwards
-          incompatibilities were introduced. Most importantly the
-          exporter no longer accepts a fixed command-line parameter to
-          specify the URL of the endpoint serving JSON. It now expects
-          this URL to be passed as an URL parameter, when scraping the
-          exporter's <literal>/probe</literal> endpoint. In the
-          prometheus scrape configuration the scrape target might look
-          like this:
-        </para>
-        <programlisting>
-http://some.json-exporter.host:7979/probe?target=https://example.com/some/json/endpoint
-</programlisting>
-        <para>
-          Existing configuration for the exporter needs to be updated,
-          but can partially be re-used. Documentation is available in
-          the upstream repository and a small example for NixOS is
-          available in the corresponding NixOS test.
-        </para>
-        <para>
-          These changes also affect
-          <link xlink:href="options.html#opt-services.prometheus.exporters.rspamd.enable">services.prometheus.exporters.rspamd.enable</link>,
-          which is just a preconfigured instance of the json exporter.
-        </para>
-        <para>
-          For more information, take a look at the
-          <link xlink:href="https://github.com/prometheus-community/json_exporter">
-          official documentation</link> of the json_exporter.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Androidenv was updated, removing the
-          <literal>includeDocs</literal> and
-          <literal>lldbVersions</literal> arguments. Docs only covered a
-          single version of the Android SDK, LLDB is now bundled with
-          the NDK, and both are no longer available to download from the
-          Android package repositories. Additionally, since the package
-          lists have been updated, some older versions of Android
-          packages may not be bundled. If you depend on older versions
-          of Android packages, we recommend overriding the repo.
-        </para>
-        <para>
-          Android packages are now loaded from a repo.json file created
-          by parsing Android repo XML files. The arguments
-          <literal>repoJson</literal> and <literal>repoXmls</literal>
-          have been added to allow overriding the built-in androidenv
-          repo.json with your own. Additionally, license files are now
-          written to allow compatibility with Gradle-based tools, and
-          the <literal>extraLicenses</literal> argument has been added
-          to accept more SDK licenses if your project requires it. See
-          the androidenv documentation for more details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The attribute <literal>mpi</literal> is now consistently used
-          to provide a default, system-wide MPI implementation. The
-          default implementation is openmpi, which has been used before
-          by all derivations affects by this change. Note that all
-          packages that have used <literal>mpi ? null</literal> in the
-          input for optional MPI builds, have been changed to the
-          boolean input paramater <literal>useMpi</literal> to enable
-          building with MPI. Building all packages with
-          <literal>mpich</literal> instead of the default
-          <literal>openmpi</literal> can now be achived like this:
-        </para>
-        <programlisting language="bash">
-self: super:
-{
-  mpi = super.mpich;
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The Searx module has been updated with the ability to
-          configure the service declaratively and uWSGI integration. The
-          option <literal>services.searx.configFile</literal> has been
-          renamed to
-          <link xlink:href="options.html#opt-services.searx.settingsFile">services.searx.settingsFile</link>
-          for consistency with the new
-          <link xlink:href="options.html#opt-services.searx.settings">services.searx.settings</link>.
-          In addition, the <literal>searx</literal> uid and gid
-          reservations have been removed since they were not necessary:
-          the service is now running with a dynamically allocated uid.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The libinput module has been updated with the ability to
-          configure mouse and touchpad settings separately. The options
-          in <literal>services.xserver.libinput</literal> have been
-          renamed to
-          <literal>services.xserver.libinput.touchpad</literal>, while
-          there is a new
-          <literal>services.xserver.libinput.mouse</literal> for mouse
-          related configuration.
-        </para>
-        <para>
-          Since touchpad options no longer apply to all devices, you may
-          want to replicate your touchpad configuration in mouse
-          section.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          ALSA OSS emulation
-          (<literal>sound.enableOSSEmulation</literal>) is now disabled
-          by default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Thinkfan as been updated to <literal>1.2.x</literal>, which
-          comes with a new YAML based configuration format. For this
-          reason, several NixOS options of the thinkfan module have been
-          changed to non-backward compatible types. In addition, a new
-          <link xlink:href="options.html#opt-services.thinkfan.settings">services.thinkfan.settings</link>
-          option has been added.
-        </para>
-        <para>
-          Please read the
-          <link xlink:href="https://github.com/vmatare/thinkfan#readme">
-          thinkfan documentation</link> before updating.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Adobe Flash Player support has been dropped from the tree. In
-          particular, the following packages no longer support it:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              chromium
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              firefox
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              qt48
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              qt5.qtwebkit
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          Additionally, packages flashplayer and hal-flash were removed
-          along with the <literal>services.flashpolicyd</literal>
-          module.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>security.rngd</literal> module has been removed.
-          It was disabled by default in 20.09 as it was functionally
-          redundant with krngd in the linux kernel. It is not necessary
-          for any device that the kernel recognises as an hardware RNG,
-          as it will automatically run the krngd task to periodically
-          collect random data from the device and mix it into the
-          kernel's RNG.
-        </para>
-        <para>
-          The default SMTP port for GitLab has been changed to
-          <literal>25</literal> from its previous default of
-          <literal>465</literal>. If you depended on this default, you
-          should now set the
-          <link xlink:href="options.html#opt-services.gitlab.smtp.port">services.gitlab.smtp.port</link>
-          option.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default version of ImageMagick has been updated from 6 to
-          7. You can use imagemagick6, imagemagick6_light, and
-          imagemagick6Big if you need the older version.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="options.html#opt-services.xserver.videoDrivers">services.xserver.videoDrivers</link>
-          no longer uses the deprecated <literal>cirrus</literal> and
-          <literal>vesa</literal> device dependent X drivers by default.
-          It also enables both <literal>amdgpu</literal> and
-          <literal>nouveau</literal> drivers by default now.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>kindlegen</literal> package is gone, because it
-          is no longer supported or hosted by Amazon. Sadly, its
-          replacement, Kindle Previewer, has no Linux support. However,
-          there are other ways to generate MOBI files. See
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/96439">the
-          discussion</link> for more info.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The apacheKafka packages are now built with version-matched
-          JREs. Versions 2.6 and above, the ones that recommend it, use
-          jdk11, while versions below remain on jdk8. The NixOS service
-          has been adjusted to start the service using the same version
-          as the package, adjustable with the new
-          <link xlink:href="options.html#opt-services.apache-kafka.jre">services.apache-kafka.jre</link>
-          option. Furthermore, the default list of
-          <link xlink:href="options.html#opt-services.apache-kafka.jvmOptions">services.apache-kafka.jvmOptions</link>
-          have been removed. You should set your own according to the
-          <link xlink:href="https://kafka.apache.org/documentation/#java">upstream
-          documentation</link> for your Kafka version.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The kodi package has been modified to allow concise addon
-          management. Consider the following configuration from previous
-          releases of NixOS to install kodi, including the
-          kodiPackages.inputstream-adaptive and kodiPackages.vfs-sftp
-          addons:
-        </para>
-        <programlisting language="bash">
-{
-  environment.systemPackages = [
-    pkgs.kodi
-  ];
-
-  nixpkgs.config.kodi = {
-    enableInputStreamAdaptive = true;
-    enableVFSSFTP = true;
-  };
-}
-</programlisting>
-        <para>
-          All Kodi <literal>config</literal> flags have been removed,
-          and as a result the above configuration should now be written
-          as:
-        </para>
-        <programlisting language="bash">
-{
-  environment.systemPackages = [
-    (pkgs.kodi.withPackages (p: with p; [
-      inputstream-adaptive
-      vfs-sftp
-    ]))
-  ];
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>environment.defaultPackages</literal> now includes
-          the nano package. If pkgs.nano is not added to the list, make
-          sure another editor is installed and the
-          <literal>EDITOR</literal> environment variable is set to it.
-          Environment variables can be set using
-          <literal>environment.variables</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.minio.dataDir</literal> changed type to a
-          list of paths, required for specifiyng multiple data
-          directories for using with erasure coding. Currently, the
-          service doesn't enforce nor checks the correct number of paths
-          to correspond to minio requirements.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          All CUDA toolkit versions prior to CUDA 10 have been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The kbdKeymaps package was removed since dvp and neo are now
-          included in kbd. If you want to use the Programmer Dvorak
-          Keyboard Layout, you have to use
-          <literal>dvorak-programmer</literal> in
-          <literal>console.keyMap</literal> now instead of
-          <literal>dvp</literal>. In
-          <literal>services.xserver.xkbVariant</literal> it's still
-          <literal>dvp</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The babeld service is now being run as an unprivileged user.
-          To achieve that the module configures
-          <literal>skip-kernel-setup true</literal> and takes care of
-          setting forwarding and rp_filter sysctls by itself as well as
-          for each interface in
-          <literal>services.babeld.interfaces</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.zigbee2mqtt.config</literal> option has
-          been renamed to
-          <literal>services.zigbee2mqtt.settings</literal> and now
-          follows
-          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
-          0042</link>.
-        </para>
-      </listitem>
-    </itemizedlist>
-    <para>
-      The yadm dotfile manager has been updated from 2.x to 3.x, which
-      has new (XDG) default locations for some data/state files. Most
-      yadm commands will fail and print a legacy path warning (which
-      describes how to upgrade/migrate your repository). If you have
-      scripts, daemons, scheduled jobs, shell profiles, etc. that invoke
-      yadm, expect them to fail or misbehave until you perform this
-      migration and prepare accordingly.
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Instead of determining
-          <literal>services.radicale.package</literal> automatically
-          based on <literal>system.stateVersion</literal>, the latest
-          version is always used because old versions are not officially
-          supported.
-        </para>
-        <para>
-          Furthermore, Radicale's systemd unit was hardened which might
-          break some deployments. In particular, a non-default
-          <literal>filesystem_folder</literal> has to be added to
-          <literal>systemd.services.radicale.serviceConfig.ReadWritePaths</literal>
-          if the deprecated <literal>services.radicale.config</literal>
-          is used.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          In the <literal>security.acme</literal> module, use of
-          <literal>--reuse-key</literal> parameter for Lego has been
-          removed. It was introduced for HKPK, but this security feature
-          is now deprecated. It is a better security practice to rotate
-          key pairs instead of always keeping the same. If you need to
-          keep this parameter, you can add it back using
-          <literal>extraLegoRenewFlags</literal> as an option for the
-          appropriate certificate.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-21.05-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>stdenv.lib</literal> has been deprecated and will
-          break eval in 21.11. Please use <literal>pkgs.lib</literal>
-          instead. See
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/108938">#108938</link>
-          for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.gnuradio.org/">GNURadio</link>
-          has a <literal>pkgs</literal> attribute set, and there's a
-          <literal>gnuradio.callPackage</literal> function that extends
-          <literal>pkgs</literal> with a
-          <literal>mkDerivation</literal>, and a
-          <literal>mkDerivationWith</literal>, like Qt5. Now all
-          <literal>gnuradio.pkgs</literal> are defined with
-          <literal>gnuradio.callPackage</literal> and some packages that
-          depend on gnuradio are defined with this as well.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.privoxy.org/">Privoxy</link> has
-          been updated to version 3.0.32 (See
-          <link xlink:href="https://lists.privoxy.org/pipermail/privoxy-announce/2021-February/000007.html">announcement</link>).
-          Compared to the previous release, Privoxy has gained support
-          for HTTPS inspection (still experimental), Brotli
-          decompression, several new filters and lots of bug fixes,
-          including security ones. In addition, the package is now built
-          with compression and external filters support, which were
-          previously disabled.
-        </para>
-        <para>
-          Regarding the NixOS module, new options for HTTPS inspection
-          have been added and
-          <literal>services.privoxy.extraConfig</literal> has been
-          replaced by the new
-          <link xlink:href="options.html#opt-services.privoxy.settings">services.privoxy.settings</link>
-          (See
-          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
-          0042</link> for the motivation).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://kodi.tv/">Kodi</link> has been
-          updated to version 19.1 &quot;Matrix&quot;. See the
-          <link xlink:href="https://kodi.tv/article/kodi-19-0-matrix-release">announcement</link>
-          for further details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.packagekit.backend</literal> option has
-          been removed as it only supported a single setting which would
-          always be the default. Instead new
-          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
-          0042</link> compliant
-          <link xlink:href="options.html#opt-services.packagekit.settings">services.packagekit.settings</link>
-          and
-          <link xlink:href="options.html#opt-services.packagekit.vendorSettings">services.packagekit.vendorSettings</link>
-          options have been introduced.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://nginx.org">Nginx</link> has been
-          updated to stable version 1.20.0. Now nginx uses the zlib-ng
-          library by default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          KDE Gear (formerly KDE Applications) is upgraded to 21.04, see
-          its
-          <link xlink:href="https://kde.org/announcements/gear/21.04/">release
-          notes</link> for details.
-        </para>
-        <para>
-          The <literal>kdeApplications</literal> package set is now
-          <literal>kdeGear</literal>, in keeping with the new name. The
-          old name remains for compatibility, but it is deprecated.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://libreswan.org/">Libreswan</link> has
-          been updated to version 4.4. The package now includes example
-          configurations and manual pages by default. The NixOS module
-          has been changed to use the upstream systemd units and write
-          the configuration in the <literal>/etc/ipsec.d/ </literal>
-          directory. In addition, two new options have been added to
-          specify connection policies
-          (<link xlink:href="options.html#opt-services.libreswan.policies">services.libreswan.policies</link>)
-          and disable send/receive redirects
-          (<link xlink:href="options.html#opt-services.libreswan.disableRedirects">services.libreswan.disableRedirects</link>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Mailman NixOS module (<literal>services.mailman</literal>)
-          has a new option
-          <link xlink:href="options.html#opt-services.mailman.enablePostfix">services.mailman.enablePostfix</link>,
-          defaulting to true, that controls integration with Postfix.
-        </para>
-        <para>
-          If this option is disabled, default MTA config becomes not set
-          and you should set the options in
-          <literal>services.mailman.settings.mta</literal> according to
-          the desired configuration as described in
-          <link xlink:href="https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html">Mailman
-          documentation</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default-version of <literal>nextcloud</literal> is
-          nextcloud21. Please note that it's <emphasis>not</emphasis>
-          possible to upgrade <literal>nextcloud</literal> across
-          multiple major versions! This means that it's e.g. not
-          possible to upgrade from nextcloud18 to nextcloud20 in a
-          single deploy and most <literal>20.09</literal> users will
-          have to upgrade to nextcloud20 first.
-        </para>
-        <para>
-          The package can be manually upgraded by setting
-          <link xlink:href="options.html#opt-services.nextcloud.package">services.nextcloud.package</link>
-          to nextcloud21.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The setting
-          <link xlink:href="options.html#opt-services.redis.bind">services.redis.bind</link>
-          defaults to <literal>127.0.0.1</literal> now, making Redis
-          listen on the loopback interface only, and not all public
-          network interfaces.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          NixOS now emits a deprecation warning if systemd's
-          <literal>StartLimitInterval</literal> setting is used in a
-          <literal>serviceConfig</literal> section instead of in a
-          <literal>unitConfig</literal>; that setting is deprecated and
-          now undocumented for the service section by systemd upstream,
-          but still effective and somewhat buggy there, which can be
-          confusing. See
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/45785">#45785</link>
-          for details.
-        </para>
-        <para>
-          All services should use
-          <link xlink:href="options.html#opt-systemd.services._name_.startLimitIntervalSec">systemd.services.<emphasis>name</emphasis>.startLimitIntervalSec</link>
-          or <literal>StartLimitIntervalSec</literal> in
-          <link xlink:href="options.html#opt-systemd.services._name_.unitConfig">systemd.services.<emphasis>name</emphasis>.unitConfig</link>
-          instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>mediatomb</literal> service declares new options.
-          It also adapts existing options so the configuration
-          generation is now lazy. The existing option
-          <literal>customCfg</literal> (defaults to false), when
-          enabled, stops the service configuration generation
-          completely. It then expects the users to provide their own
-          correct configuration at the right location (whereas the
-          configuration was generated and not used at all before). The
-          new option <literal>transcodingOption</literal> (defaults to
-          no) allows a generated configuration. It makes the mediatomb
-          service pulls the necessary runtime dependencies in the nix
-          store (whereas it was generated with hardcoded values before).
-          The new option <literal>mediaDirectories</literal> allows the
-          users to declare autoscan media directories from their nixos
-          configuration:
-        </para>
-        <programlisting language="bash">
-{
-  services.mediatomb.mediaDirectories = [
-    { path = &quot;/var/lib/mediatomb/pictures&quot;; recursive = false; hidden-files = false; }
-    { path = &quot;/var/lib/mediatomb/audio&quot;; recursive = true; hidden-files = false; }
-  ];
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The Unbound DNS resolver service
-          (<literal>services.unbound</literal>) has been refactored to
-          allow reloading, control sockets and to fix startup ordering
-          issues.
-        </para>
-        <para>
-          It is now possible to enable a local UNIX control socket for
-          unbound by setting the
-          <link xlink:href="options.html#opt-services.unbound.localControlSocketPath">services.unbound.localControlSocketPath</link>
-          option.
-        </para>
-        <para>
-          Previously we just applied a very minimal set of restrictions
-          and trusted unbound to properly drop root privs and
-          capabilities.
-        </para>
-        <para>
-          As of this we are (for the most part) just using the upstream
-          example unit file for unbound. The main difference is that we
-          start unbound as <literal>unbound</literal> user with the
-          required capabilities instead of letting unbound do the chroot
-          &amp; uid/gid changes.
-        </para>
-        <para>
-          The upstream unit configuration this is based on is a lot
-          stricter with all kinds of permissions then our previous
-          variant. It also came with the default of having the
-          <literal>Type</literal> set to <literal>notify</literal>,
-          therefore we are now also using the
-          <literal>unbound-with-systemd</literal> package here. Unbound
-          will start up, read the configuration files and start
-          listening on the configured ports before systemd will declare
-          the unit <literal>active (running)</literal>. This will likely
-          help with startup order and the occasional race condition
-          during system activation where the DNS service is started but
-          not yet ready to answer queries. Services depending on
-          <literal>nss-lookup.target</literal> or
-          <literal>unbound.service</literal> are now be able to use
-          unbound when those targets have been reached.
-        </para>
-        <para>
-          Additionally to the much stricter runtime environment the
-          <literal>/dev/urandom</literal> mount lines we previously had
-          in the code (that randomly failed during the stop-phase) have
-          been removed as systemd will take care of those for us.
-        </para>
-        <para>
-          The <literal>preStart</literal> script is now only required if
-          we enabled the trust anchor updates (which are still enabled
-          by default).
-        </para>
-        <para>
-          Another benefit of the refactoring is that we can now issue
-          reloads via either <literal>pkill -HUP unbound</literal> and
-          <literal>systemctl reload unbound</literal> to reload the
-          running configuration without taking the daemon offline. A
-          prerequisite of this was that unbound configuration is
-          available on a well known path on the file system. We are
-          using the path <literal>/etc/unbound/unbound.conf</literal> as
-          that is the default in the CLI tooling which in turn enables
-          us to use <literal>unbound-control</literal> without passing a
-          custom configuration location.
-        </para>
-        <para>
-          The module has also been reworked to be
-          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
-          0042</link> compliant. As such,
-          <literal>sevices.unbound.extraConfig</literal> has been
-          removed and replaced by
-          <link xlink:href="options.html#opt-services.unbound.settings">services.unbound.settings</link>.
-          <literal>services.unbound.interfaces</literal> has been
-          renamed to
-          <literal>services.unbound.settings.server.interface</literal>.
-        </para>
-        <para>
-          <literal>services.unbound.forwardAddresses</literal> and
-          <literal>services.unbound.allowedAccess</literal> have also
-          been changed to use the new settings interface. You can follow
-          the instructions when executing
-          <literal>nixos-rebuild</literal> to upgrade your configuration
-          to use the new interface.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.dnscrypt-proxy2</literal> module now
-          takes the upstream's example configuration and updates it with
-          the user's settings. An option has been added to restore the
-          old behaviour if you prefer to declare the configuration from
-          scratch.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          NixOS now defaults to the unified cgroup hierarchy
-          (cgroupsv2). See the
-          <link xlink:href="https://www.redhat.com/sysadmin/fedora-31-control-group-v2">Fedora
-          Article for 31</link> for details on why this is desirable,
-          and how it impacts containers.
-        </para>
-        <para>
-          If you want to run containers with a runtime that does not yet
-          support cgroupsv2, you can switch back to the old behaviour by
-          setting
-          <link xlink:href="options.html#opt-systemd.enableUnifiedCgroupHierarchy">systemd.enableUnifiedCgroupHierarchy</link>
-          = <literal>false</literal>; and rebooting.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PulseAudio was upgraded to 14.0, with changes to the handling
-          of default sinks. See its
-          <link xlink:href="https://www.freedesktop.org/wiki/Software/PulseAudio/Notes/14.0/">release
-          notes</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          GNOME users may wish to delete their
-          <literal>~/.config/pulse</literal> due to the changes to
-          stream routing logic. See
-          <link xlink:href="https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/832">PulseAudio
-          bug 832</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The zookeeper package does not provide
-          <literal>zooInspector.sh</literal> anymore, as that
-          &quot;contrib&quot; has been dropped from upstream releases.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          In the ACME module, the data used to build the hash for the
-          account directory has changed to accommodate new features to
-          reduce account rate limit issues. This will trigger new
-          account creation on the first rebuild following this update.
-          No issues are expected to arise from this, thanks to the new
-          account creation handling.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="options.html#opt-users.users._name_.createHome">users.users.<emphasis>name</emphasis>.createHome</link>
-          now always ensures home directory permissions to be
-          <literal>0700</literal>. Permissions had previously been
-          ignored for already existing home directories, possibly
-          leaving them readable by others. The option's description was
-          incorrect regarding ownership management and has been
-          simplified greatly.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          When defining a new user, one of
-          <link xlink:href="options.html#opt-users.users._name_.isNormalUser">users.users.<emphasis>name</emphasis>.isNormalUser</link>
-          and
-          <link xlink:href="options.html#opt-users.users._name_.isSystemUser">users.users.<emphasis>name</emphasis>.isSystemUser</link>
-          is now required. This is to prevent accidentally giving a UID
-          above 1000 to system users, which could have unexpected
-          consequences, like running user activation scripts for system
-          users. Note that users defined with an explicit UID below 500
-          are exempted from this check, as
-          <link xlink:href="options.html#opt-users.users._name_.isSystemUser">users.users.<emphasis>name</emphasis>.isSystemUser</link>
-          has no effect for those.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>security.apparmor</literal> module, for the
-          <link xlink:href="https://gitlab.com/apparmor/apparmor/-/wikis/Documentation">AppArmor</link>
-          Mandatory Access Control system, has been substantialy
-          improved along with related tools, so that module maintainers
-          can now more easily write AppArmor profiles for NixOS. The
-          most notable change on the user-side is the new option
-          <link xlink:href="options.html#opt-security.apparmor.policies">security.apparmor.policies</link>,
-          replacing the previous <literal>profiles</literal> option to
-          provide a way to disable a profile and to select whether to
-          confine in enforce mode (default) or in complain mode (see
-          <literal>journalctl -b --grep apparmor</literal>).
-          Security-minded users may also want to enable
-          <link xlink:href="options.html#opt-security.apparmor.killUnconfinedConfinables">security.apparmor.killUnconfinedConfinables</link>,
-          at the cost of having some of their processes killed when
-          updating to a NixOS version introducing new AppArmor profiles.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The GNOME desktop manager once again installs gnome.epiphany
-          by default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          NixOS now generates empty <literal>/etc/netgroup</literal>.
-          <literal>/etc/netgroup</literal> defines network-wide groups
-          and may affect to setups using NIS.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Platforms, like <literal>stdenv.hostPlatform</literal>, no
-          longer have a <literal>platform</literal> attribute. It has
-          been (mostly) flattened away:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>platform.gcc</literal> is now
-              <literal>gcc</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>platform.kernel*</literal> is now
-              <literal>linux-kernel.*</literal>
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          Additionally, <literal>platform.kernelArch</literal> moved to
-          the top level as <literal>linuxArch</literal> to match the
-          other <literal>*Arch</literal> variables.
-        </para>
-        <para>
-          The <literal>platform</literal> grouping of these things never
-          meant anything, and was just a historial/implementation
-          artifact that was overdue removal.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.restic</literal> now uses a dedicated cache
-          directory for every backup defined in
-          <literal>services.restic.backups</literal>. The old global
-          cache directory, <literal>/root/.cache/restic</literal>, is
-          now unused and can be removed to free up disk space.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>isync</literal>: The <literal>isync</literal>
-          compatibility wrapper was removed and the Master/Slave
-          terminology has been deprecated and should be replaced with
-          Far/Near in the configuration file.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The nix-gc service now accepts randomizedDelaySec (default: 0)
-          and persistent (default: true) parameters. By default nix-gc
-          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>
-          The <literal>rustPlatform.buildRustPackage</literal> function
-          is split into several hooks: cargoSetupHook to set up
-          vendoring for Cargo-based projects, cargoBuildHook to build a
-          project using Cargo, cargoInstallHook to install a project
-          using Cargo, and cargoCheckHook to run tests in Cargo-based
-          projects. With this change, mixed-language projects can use
-          the relevant hooks within builders other than
-          <literal>buildRustPackage</literal>. However, these changes
-          also required several API changes to
-          <literal>buildRustPackage</literal> itself:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              The <literal>target</literal> argument was removed.
-              Instead, <literal>buildRustPackage</literal> will always
-              use the same target as the C/C++ compiler that is used.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The <literal>cargoParallelTestThreads</literal> argument
-              was removed. Parallel tests are now disabled through
-              <literal>dontUseCargoParallelTests</literal>.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>rustPlatform.maturinBuildHook</literal> hook was
-          added. This hook can be used with
-          <literal>buildPythonPackage</literal> to build Python packages
-          that are written in Rust and use Maturin as their build tool.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Kubernetes has
-          <link xlink:href="https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/">deprecated
-          docker</link> as container runtime. As a consequence, the
-          Kubernetes module now has support for configuration of custom
-          remote container runtimes and enables containerd by default.
-          Note that containerd is more strict regarding container image
-          OCI-compliance. As an example, images with CMD or ENTRYPOINT
-          defined as strings (not lists) will fail on containerd, while
-          working fine on docker. Please test your setup and container
-          images with containerd prior to upgrading.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The GitLab module now has support for automatic backups. A
-          schedule can be set with the
-          <link xlink:href="options.html#opt-services.gitlab.backup.startAt">services.gitlab.backup.startAt</link>
-          option.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Prior to this release, systemd would also read system units
-          from an undocumented
-          <literal>/etc/systemd-mutable/system</literal> path. This path
-          has been dropped from the defaults. That path (or others) can
-          be re-enabled by adding it to the
-          <link xlink:href="options.html#opt-boot.extraSystemdUnitPaths">boot.extraSystemdUnitPaths</link>
-          list.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PostgreSQL 9.5 is scheduled EOL during the 21.05 life cycle
-          and has been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.xfce.org/">Xfce4</link> relies
-          on GIO/GVfs for userspace virtual filesystem access in
-          applications like
-          <link xlink:href="https://docs.xfce.org/xfce/thunar/">thunar</link>
-          and
-          <link xlink:href="https://docs.xfce.org/apps/gigolo/">gigolo</link>.
-          For that to work, the gvfs nixos service is enabled by
-          default, and it can be configured with the specific package
-          that provides GVfs. Until now Xfce4 was setting it to use a
-          lighter version of GVfs (without support for samba). To avoid
-          conflicts with other desktop environments this setting has
-          been dropped. Users that still want it should add the
-          following to their system configuration:
-        </para>
-        <programlisting language="bash">
-{
-  services.gvfs.package = pkgs.gvfs.override { samba = null; };
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          The newly enabled <literal>systemd-pstore.service</literal>
-          now automatically evacuates crashdumps and panic logs from the
-          persistent storage to
-          <literal>/var/lib/systemd/pstore</literal>. This prevents
-          NVRAM from filling up, which ensures the latest diagnostic
-          data is always stored and alleviates problems with writing new
-          boot configurations.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Nixpkgs now contains
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/118232">automatically
-          packaged GNOME Shell extensions</link> from the
-          <link xlink:href="https://extensions.gnome.org/">GNOME
-          Extensions</link> portal. You can find them, filed by their
-          UUID, under <literal>gnome38Extensions</literal> attribute for
-          GNOME 3.38 and under <literal>gnome40Extensions</literal> for
-          GNOME 40. Finally, the <literal>gnomeExtensions</literal>
-          attribute contains extensions for the latest GNOME Shell
-          version in Nixpkgs, listed under a more human-friendly name.
-          The unqualified attribute scope also contains manually
-          packaged extensions. Note that the automatically packaged
-          extensions are provided for convenience and are not checked or
-          guaranteed to work.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Erlang/OTP versions older than R21 got dropped. We also
-          dropped the cuter package, as it was purely an example of how
-          to build a package. We also dropped <literal>lfe_1_2</literal>
-          as it could not build with R21+. Moving forward, we expect to
-          only support 3 yearly releases of OTP.
-        </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
deleted file mode 100644
index 9b6e755fd47..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
+++ /dev/null
@@ -1,2122 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-21.11">
-  <title>Release 21.11 (“Porcupine”, 2021/11/30)</title>
-  <itemizedlist spacing="compact">
-    <listitem>
-      <para>
-        Support is planned until the end of June 2022, handing over to
-        22.05.
-      </para>
-    </listitem>
-  </itemizedlist>
-  <section xml:id="sec-release-21.11-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      has the following highlights:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Nix has been updated to version 2.4, reference its
-          <link xlink:href="https://discourse.nixos.org/t/nix-2-4-released/15822">release
-          notes</link> for more information on what has changed. The
-          previous version of Nix, 2.3.16, remains available for the
-          time being in the <literal>nix_2_3</literal> package.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <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>
-        <para>
-          PHP now defaults to PHP 8.0, updated from 7.4.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          kops now defaults to 1.21.1, which uses containerd as the
-          default runtime.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>python3</literal> now defaults to Python 3.9, updated
-          from Python 3.8.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PostgreSQL now defaults to major version 13.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          spark now defaults to spark 3, updated from 2. A
-          <link xlink:href="https://spark.apache.org/docs/latest/core-migration-guide.html#upgrading-from-core-24-to-30">migration
-          guide</link> is available.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Improvements have been made to the Hadoop module and package:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              HDFS and YARN now support production-ready highly
-              available deployments with automatic failover.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Hadoop now defaults to Hadoop 3, updated from 2.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              JournalNode, ZKFS and HTTPFS services have been added.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Activation scripts can now, optionally, be run during a
-          <literal>nixos-rebuild dry-activate</literal> and can detect
-          the dry activation by reading
-          <literal>$NIXOS_ACTION</literal>. This allows activation
-          scripts to output what they would change if the activation was
-          really run. The users/modules activation script supports this
-          and outputs some of is actions.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          KDE Plasma now finally works on Wayland.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          bash now defaults to major version 5.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Systemd was updated to version 249 (from 247).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Pantheon desktop has been updated to version 6. Due to changes
-          of screen locker, if locking doesn’t work for you, please try
-          <literal>gsettings set org.gnome.desktop.lockdown disable-lock-screen false</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>kubernetes-helm</literal> now defaults to 3.7.0,
-          which introduced some breaking changes to the experimental OCI
-          manifest format. See
-          <link xlink:href="https://github.com/helm/community/blob/main/hips/hip-0006.md">HIP
-          6</link> for more details. <literal>helmfile</literal> also
-          defaults to 0.141.0, which is the minimum compatible version.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          GNOME has been upgraded to 41. Please take a look at their
-          <link xlink:href="https://help.gnome.org/misc/release-notes/41.0/">Release
-          Notes</link> for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          LXD support was greatly improved:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              building LXD images from configurations is now directly
-              possible with just nixpkgs
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              hydra is now building nixOS LXD images that can be used
-              standalone with full nixos-rebuild support
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          OpenSSH was updated to version 8.8p1
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              This breaks connections to old SSH daemons as ssh-rsa host
-              keys and ssh-rsa public keys that were signed with SHA-1
-              are disabled by default now
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              These can be re-enabled, see the
-              <link xlink:href="https://www.openssh.com/txt/release-8.8">OpenSSH
-              changelog</link> for details
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          ORY Kratos was updated to version 0.8.0-alpha.3
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              This release requires you to run SQL migrations. Please,
-              as always, create a backup of your database first!
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The SDKs are now generated with tag v0alpha2 to reflect
-              that some signatures have changed in a breaking fashion.
-              Please update your imports from v0alpha1 to v0alpha2.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The SMTPS scheme used in courier config URL with
-              cleartext/StartTLS/TLS SMTP connection types is now only
-              supporting implicit TLS. For StartTLS and cleartext SMTP,
-              please use the SMTP scheme instead.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              for more details, see
-              <link xlink:href="https://github.com/ory/kratos/releases/tag/v0.8.0-alpha.1">Release
-              Notes</link>.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-21.11-new-services">
-    <title>New Services</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <link xlink:href="https://digint.ch/btrbk/index.html">btrbk</link>,
-          a backup tool for btrfs subvolumes, taking advantage of btrfs
-          specific capabilities to create atomic snapshots and transfer
-          them incrementally to your backup locations. Available as
-          <link xlink:href="options.html#opt-services.brtbk.instances">services.btrbk</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/xrelkd/clipcat/">clipcat</link>,
-          an X11 clipboard manager written in Rust. Available at
-          <link xlink:href="options.html#opt-services.clipcat.enable">services.clipcat</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/dexidp/dex">dex</link>,
-          an OpenID Connect (OIDC) identity and OAuth 2.0 provider.
-          Available at
-          <link xlink:href="options.html#opt-services.dex.enable">services.dex</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/maxmind/geoipupdate">geoipupdate</link>,
-          a GeoIP database updater from MaxMind. Available as
-          <link xlink:href="options.html#opt-services.geoipupdate.enable">services.geoipupdate</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/jitsi/jibri">Jibri</link>,
-          a service for recording or streaming a Jitsi Meet conference.
-          Available as
-          <link xlink:href="options.html#opt-services.jibri.enable">services.jibri</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.isc.org/kea/">Kea</link>, ISCs
-          2nd generation DHCP and DDNS server suite. Available at
-          <link xlink:href="options.html#opt-services.kea.dhcp4">services.kea</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://owncast.online/">owncast</link>,
-          self-hosted video live streaming solution. Available at
-          <link xlink:href="options.html#opt-services.owncast.enable">services.owncast</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://joinpeertube.org/">PeerTube</link>,
-          developed by Framasoft, is the free and decentralized
-          alternative to video platforms. Available at
-          <link xlink:href="options.html#opt-services.peertube.enable">services.peertube</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://sr.ht">sourcehut</link>, a
-          collection of tools useful for software development. Available
-          as
-          <link xlink:href="options.html#opt-services.sourcehut.enable">services.sourcehut</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://download.pureftpd.org/pub/ucarp/README">ucarp</link>,
-          an userspace implementation of the Common Address Redundancy
-          Protocol (CARP). Available as
-          <link xlink:href="options.html#opt-networking.ucarp.enable">networking.ucarp</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Users of flashrom should migrate to
-          <link xlink:href="options.html#opt-programs.flashrom.enable">programs.flashrom.enable</link>
-          and add themselves to the <literal>flashrom</literal> group to
-          be able to access programmers supported by flashrom.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://vikunja.io">vikunja</link>, a to-do
-          list app. Available as
-          <link linkend="opt-services.vikunja.enable">services.vikunja</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/evilsocket/opensnitch">opensnitch</link>,
-          an application firewall. Available as
-          <link linkend="opt-services.opensnitch.enable">services.opensnitch</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.snapraid.it/">snapraid</link>, a
-          backup program for disk arrays. Available as
-          <link linkend="opt-snapraid.enable">snapraid</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/hockeypuck/hockeypuck">Hockeypuck</link>,
-          a OpenPGP Key Server. Available as
-          <link linkend="opt-services.hockeypuck.enable">services.hockeypuck</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/buildkite/buildkite-agent-metrics">buildkite-agent-metrics</link>,
-          a command-line tool for collecting Buildkite agent metrics,
-          now has a Prometheus exporter available as
-          <link linkend="opt-services.prometheus.exporters.buildkite-agent.enable">services.prometheus.exporters.buildkite-agent</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/prometheus/influxdb_exporter">influxdb-exporter</link>
-          a Prometheus exporter that exports metrics received on an
-          InfluxDB compatible endpoint is now available as
-          <link linkend="opt-services.prometheus.exporters.influxdb.enable">services.prometheus.exporters.influxdb</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/matrix-discord/mx-puppet-discord">mx-puppet-discord</link>,
-          a discord puppeting bridge for matrix. Available as
-          <link linkend="opt-services.mx-puppet-discord.enable">services.mx-puppet-discord</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.meshcommander.com/meshcentral2/overview">MeshCentral</link>,
-          a remote administration service (<quote>TeamViewer but
-          self-hosted and with more features</quote>) is now available
-          with a package and a module:
-          <link linkend="opt-services.meshcentral.enable">services.meshcentral.enable</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/Arksine/moonraker">moonraker</link>,
-          an API web server for Klipper. Available as
-          <link linkend="opt-services.moonraker.enable">moonraker</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/influxdata/influxdb">influxdb2</link>,
-          a Scalable datastore for metrics, events, and real-time
-          analytics. Available as
-          <link linkend="opt-services.influxdb2.enable">services.influxdb2</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://posativ.org/isso/">isso</link>, a
-          commenting server similar to Disqus. Available as
-          <link linkend="opt-services.isso.enable">isso</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.navidrome.org/">navidrome</link>,
-          a personal music streaming server with subsonic-compatible
-          api. Available as
-          <link linkend="opt-services.navidrome.enable">navidrome</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://docs.fluidd.xyz/">fluidd</link>, a
-          Klipper web interface for managing 3d printers using
-          moonraker. Available as
-          <link linkend="opt-services.fluidd.enable">fluidd</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/earnestly/sx">sx</link>,
-          a simple alternative to both xinit and startx for starting a
-          Xorg server. Available as
-          <link linkend="opt-services.xserver.displayManager.sx.enable">services.xserver.displayManager.sx</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://postfixadmin.sourceforge.io/">postfixadmin</link>,
-          a web based virtual user administration interface for Postfix
-          mail servers. Available as
-          <link linkend="opt-services.postfixadmin.enable">postfixadmin</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://wiki.servarr.com/prowlarr">prowlarr</link>,
-          an indexer manager/proxy built on the popular arr .net/reactjs
-          base stack
-          <link linkend="opt-services.prowlarr.enable">services.prowlarr</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://sr.ht/~emersion/soju">soju</link>, a
-          user-friendly IRC bouncer. Available as
-          <link xlink:href="options.html#opt-services.soju.enable">services.soju</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://nats.io/">nats</link>, a high
-          performance cloud and edge messaging system. Available as
-          <link linkend="opt-services.nats.enable">services.nats</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://git-scm.com">git</link>, a
-          distributed version control system. Available as
-          <link xlink:href="options.html#opt-programs.git.enable">programs.git</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://domainaware.github.io/parsedmarc/">parsedmarc</link>,
-          a service which parses incoming
-          <link xlink:href="https://dmarc.org/">DMARC</link> reports and
-          stores or sends them to a downstream service for further
-          analysis. Documented in
-          <link linkend="module-services-parsedmarc">its manual
-          entry</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://spark.apache.org/">spark</link>, a
-          unified analytics engine for large-scale data processing.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/JoseExposito/touchegg">touchegg</link>,
-          a multi-touch gesture recognizer. Available as
-          <link linkend="opt-services.touchegg.enable">services.touchegg</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/pantheon-tweaks/pantheon-tweaks">pantheon-tweaks</link>,
-          an unofficial system settings panel for Pantheon. Available as
-          <link linkend="opt-programs.pantheon-tweaks.enable">programs.pantheon-tweaks</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/DanielOgorchock/joycond">joycond</link>,
-          a service that uses <literal>hid-nintendo</literal> to provide
-          nintendo joycond pairing and better nintendo switch pro
-          controller support.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/opensvc/multipath-tools">multipath</link>,
-          the device mapper multipath (DM-MP) daemon. Available as
-          <link linkend="opt-services.multipath.enable">services.multipath</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.seafile.com/en/home/">seafile</link>,
-          an open source file syncing &amp; sharing software. Available
-          as
-          <link xlink:href="options.html#opt-services.seafile.enable">services.seafile</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/mchehab/rasdaemon">rasdaemon</link>,
-          a hardware error logging daemon. Available as
-          <link linkend="opt-hardware.rasdaemon.enable">hardware.rasdaemon</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>code-server</literal>-module now available
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/xmrig/xmrig">xmrig</link>,
-          a high performance, open source, cross platform RandomX,
-          KawPow, CryptoNight and AstroBWT unified CPU/GPU miner and
-          RandomX benchmark.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Auto nice daemons
-          <link xlink:href="https://github.com/Nefelim4ag/Ananicy">ananicy</link>
-          and
-          <link xlink:href="https://gitlab.com/ananicy-cpp/ananicy-cpp/">ananicy-cpp</link>.
-          Available as
-          <link linkend="opt-services.ananicy.enable">services.ananicy</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/prometheus-community/smartctl_exporter">smartctl_exporter</link>,
-          a Prometheus exporter for
-          <link xlink:href="https://en.wikipedia.org/wiki/S.M.A.R.T.">S.M.A.R.T.</link>
-          data. Available as
-          <link xlink:href="options.html#opt-services.prometheus.exporters.smartctl.enable">services.prometheus.exporters.smartctl</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://docs.twingate.com/docs/linux">twingate</link>,
-          a high performance, easy to use zero trust solution that
-          enables access to private resources from any device with
-          better security than a VPN.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-21.11-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The NixOS VM test framework,
-          <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
-          <literal>succeed(&quot;foo &gt;&amp;2 &amp;&quot;)</literal>.
-          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
-          <literal>execute</literal>, <literal>succeed</literal>,
-          <literal>fail</literal>,
-          <literal>wait_until_succeeds</literal>,
-          <literal>wait_until_fails</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.wakeonlan</literal> option was removed,
-          and replaced with
-          <literal>networking.interfaces.&lt;name&gt;.wakeOnLan</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>security.wrappers</literal> option now requires
-          to always specify an owner, group and whether the
-          setuid/setgid bit should be set. This is motivated by the fact
-          that before NixOS 21.11, specifying either setuid or setgid
-          but not owner/group resulted in wrappers owned by
-          nobody/nogroup, which is unsafe.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Since <literal>iptables</literal> now uses
-          <literal>nf_tables</literal> backend and
-          <literal>ipset</literal> doesn’t support it, some applications
-          (ferm, shorewall, firehol) may have limited functionality.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>paperless</literal> module and package have been
-          removed. All users should migrate to the successor
-          <literal>paperless-ng</literal> instead. The Paperless project
-          <link xlink:href="https://github.com/the-paperless-project/paperless/commit/9b0063c9731f7c5f65b1852cb8caff97f5e40ba4">has
-          been archived</link> and advises all users to use
-          <literal>paperless-ng</literal> instead.
-        </para>
-        <para>
-          Users can use the <literal>services.paperless-ng</literal>
-          module as a replacement while noting the following
-          incompatibilities:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              <literal>services.paperless.ocrLanguages</literal> has no
-              replacement. Users should migrate to
-              <link xlink:href="options.html#opt-services.paperless-ng.extraConfig"><literal>services.paperless-ng.extraConfig</literal></link>
-              instead:
-            </para>
-          </listitem>
-        </itemizedlist>
-        <programlisting language="bash">
-{
-  services.paperless-ng.extraConfig = {
-    # Provide languages as ISO 639-2 codes
-    # separated by a plus (+) sign.
-    # https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes
-    PAPERLESS_OCR_LANGUAGE = &quot;deu+eng+jpn&quot;; # German &amp; English &amp; Japanse
-  };
-}
-</programlisting>
-        <itemizedlist>
-          <listitem>
-            <para>
-              If you previously specified
-              <literal>PAPERLESS_CONSUME_MAIL_*</literal> settings in
-              <literal>services.paperless.extraConfig</literal> you
-              should remove those options now. You now
-              <emphasis>must</emphasis> define those settings in the
-              admin interface of paperless-ng.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Option <literal>services.paperless.manage</literal> no
-              longer exists. Use the script at
-              <literal>${services.paperless-ng.dataDir}/paperless-ng-manage</literal>
-              instead. Note that this script only exists after the
-              <literal>paperless-ng</literal> service has been started
-              at least once.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              After switching to the new system configuration you should
-              run the Django management command to reindex your
-              documents and optionally create a user, if you don’t have
-              one already.
-            </para>
-            <para>
-              To do so, enter the data directory (the value of
-              <literal>services.paperless-ng.dataDir</literal>,
-              <literal>/var/lib/paperless</literal> by default), switch
-              to the paperless user and execute the management command
-              like below:
-            </para>
-            <programlisting>
-$ cd /var/lib/paperless
-$ su paperless -s /bin/sh
-$ ./paperless-ng-manage document_index reindex
-# if not already done create a user account, paperless-ng requires a login
-$ ./paperless-ng-manage createsuperuser
-Username (leave blank to use 'paperless'): my-user-name
-Email address: me@example.com
-Password: **********
-Password (again): **********
-Superuser created successfully.
-</programlisting>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>staticjinja</literal> package has been upgraded
-          from 1.0.4 to 4.1.1
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Firefox v91 does not support addons with invalid signature
-          anymore. Firefox ESR needs to be used for nix addon support.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>erigon</literal> ethereum node has moved to a new
-          database format in <literal>2021-05-04</literal>, and requires
-          a full resync
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>erigon</literal> ethereum node has moved it’s
-          database location in <literal>2021-08-03</literal>, users
-          upgrading must manually move their chaindata (see
-          <link xlink:href="https://github.com/ledgerwatch/erigon/releases/tag/v2021.08.03">release
-          notes</link>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="options.html#opt-users.users._name_.group">users.users.&lt;name&gt;.group</link>
-          no longer defaults to <literal>nogroup</literal>, which was
-          insecure. Out-of-tree modules are likely to require
-          adaptation: instead of
-        </para>
-        <programlisting language="bash">
-{
-  users.users.foo = {
-    isSystemUser = true;
-  };
-}
-</programlisting>
-        <para>
-          also create a group for your user:
-        </para>
-        <programlisting language="bash">
-{
-  users.users.foo = {
-    isSystemUser = true;
-    group = &quot;foo&quot;;
-  };
-  users.groups.foo = {};
-}
-</programlisting>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.geoip-updater</literal> was broken and has
-          been replaced by
-          <link xlink:href="options.html#opt-services.geoipupdate.enable">services.geoipupdate</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>ihatemoney</literal> has been updated to version
-          5.1.1
-          (<link xlink:href="https://github.com/spiral-project/ihatemoney/blob/5.1.1/CHANGELOG.rst">release
-          notes</link>). If you serve ihatemoney by HTTP rather than
-          HTTPS, you must set
-          <link xlink:href="options.html#opt-services.ihatemoney.secureCookie">services.ihatemoney.secureCookie</link>
-          to <literal>false</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PHP 7.3 is no longer supported due to upstream not supporting
-          this version for the entire lifecycle of the 21.11 release.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Those making use of <literal>buildBazelPackage</literal> will
-          need to regenerate the fetch hashes (preferred), or set
-          <literal>fetchConfigured = false;</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>consul</literal> was upgraded to a new major release
-          with breaking changes, see
-          <link xlink:href="https://github.com/hashicorp/consul/releases/tag/v1.10.0">upstream
-          changelog</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          fsharp41 has been removed in preference to use the latest
-          dotnet-sdk
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The following F#-related packages have been removed for being
-          unmaintaned. Please use <literal>fetchNuGet</literal> for
-          specific packages.
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              ExtCore
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Fake
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Fantomas
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FsCheck
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FsCheck262
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FsCheckNunit
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpAutoComplete
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpCompilerCodeDom
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpCompilerService
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpCompilerTools
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpCore302
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpCore3125
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpCore4001
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpCore4117
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpData
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpData225
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpDataSQLProvider
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FSharpFormatting
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FsLexYacc
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FsLexYacc706
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FsLexYaccRuntime
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FsPickler
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              FsUnit
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Projekt
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Suave
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              UnionArgParser
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              ExcelDnaRegistration
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              MathNetNumerics
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs.x2goserver</literal> is now
-          <literal>services.x2goserver</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The following dotnet-related packages have been removed for
-          being unmaintaned. Please use <literal>fetchNuGet</literal>
-          for specific packages.
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              Autofac
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              SystemValueTuple
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              MicrosoftDiaSymReader
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              MicrosoftDiaSymReaderPortablePdb
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              SystemCollectionsImmutable
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              SystemCollectionsImmutable131
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              SystemReflectionMetadata
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              NUnit350
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Deedle
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              ExcelDna
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              GitVersionTree
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              NDeskOptions
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-    </itemizedlist>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The <literal>antlr</literal> package now defaults to the 4.x
-          release instead of the old 2.7.7 version.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>pulseeffects</literal> package updated to
-          <link xlink:href="https://github.com/wwmm/easyeffects/releases/tag/v6.0.0">version
-          4.x</link> and renamed to <literal>easyeffects</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>libwnck</literal> package now defaults to the 3.x
-          release instead of the old 2.31.0 version.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>bitwarden_rs</literal> packages and modules were
-          renamed to <literal>vaultwarden</literal>
-          <link xlink:href="https://github.com/dani-garcia/vaultwarden/discussions/1642">following
-          upstream</link>. More specifically,
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              <literal>pkgs.bitwarden_rs</literal>,
-              <literal>pkgs.bitwarden_rs-sqlite</literal>,
-              <literal>pkgs.bitwarden_rs-mysql</literal> and
-              <literal>pkgs.bitwarden_rs-postgresql</literal> were
-              renamed to <literal>pkgs.vaultwarden</literal>,
-              <literal>pkgs.vaultwarden-sqlite</literal>,
-              <literal>pkgs.vaultwarden-mysql</literal> and
-              <literal>pkgs.vaultwarden-postgresql</literal>,
-              respectively.
-            </para>
-            <itemizedlist spacing="compact">
-              <listitem>
-                <para>
-                  Old names are preserved as aliases for backwards
-                  compatibility, but may be removed in the future.
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  The <literal>bitwarden_rs</literal> executable was
-                  also renamed to <literal>vaultwarden</literal> in all
-                  packages.
-                </para>
-              </listitem>
-            </itemizedlist>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>pkgs.bitwarden_rs-vault</literal> was renamed to
-              <literal>pkgs.vaultwarden-vault</literal>.
-            </para>
-            <itemizedlist spacing="compact">
-              <listitem>
-                <para>
-                  <literal>pkgs.bitwarden_rs-vault</literal> is
-                  preserved as an alias for backwards compatibility, but
-                  may be removed in the future.
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  The static files were moved from
-                  <literal>/usr/share/bitwarden_rs</literal> to
-                  <literal>/usr/share/vaultwarden</literal>.
-                </para>
-              </listitem>
-            </itemizedlist>
-          </listitem>
-          <listitem>
-            <para>
-              The <literal>services.bitwarden_rs</literal> config module
-              was renamed to <literal>services.vaultwarden</literal>.
-            </para>
-            <itemizedlist spacing="compact">
-              <listitem>
-                <para>
-                  <literal>services.bitwarden_rs</literal> is preserved
-                  as an alias for backwards compatibility, but may be
-                  removed in the future.
-                </para>
-              </listitem>
-            </itemizedlist>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>systemd.services.bitwarden_rs</literal>,
-              <literal>systemd.services.backup-bitwarden_rs</literal>
-              and <literal>systemd.timers.backup-bitwarden_rs</literal>
-              were renamed to
-              <literal>systemd.services.vaultwarden</literal>,
-              <literal>systemd.services.backup-vaultwarden</literal> and
-              <literal>systemd.timers.backup-vaultwarden</literal>,
-              respectively.
-            </para>
-            <itemizedlist spacing="compact">
-              <listitem>
-                <para>
-                  Old names are preserved as aliases for backwards
-                  compatibility, but may be removed in the future.
-                </para>
-              </listitem>
-            </itemizedlist>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>users.users.bitwarden_rs</literal> and
-              <literal>users.groups.bitwarden_rs</literal> were renamed
-              to <literal>users.users.vaultwarden</literal> and
-              <literal>users.groups.vaultwarden</literal>, respectively.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The data directory remains located at
-              <literal>/var/lib/bitwarden_rs</literal>, for backwards
-              compatibility.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-    </itemizedlist>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>yggdrasil</literal> was upgraded to a new major
-          release with breaking changes, see
-          <link xlink:href="https://github.com/yggdrasil-network/yggdrasil-go/releases/tag/v0.4.0">upstream
-          changelog</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>icingaweb2</literal> was upgraded to a new release
-          which requires a manual database upgrade, see
-          <link xlink:href="https://github.com/Icinga/icingaweb2/releases/tag/v2.9.0">upstream
-          changelog</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>isabelle</literal> package has been upgraded from
-          2020 to 2021
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          the <literal>mingw-64</literal> package has been upgraded from
-          6.0.0 to 9.0.0
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>tt-rss</literal> was upgraded to the commit on
-          2021-06-21, which has breaking changes. If you use
-          <literal>services.tt-rss.extraConfig</literal> you should
-          migrate to the <literal>putenv</literal>-style configuration.
-          See
-          <link xlink:href="https://community.tt-rss.org/t/rip-config-php-hello-classes-config-php/4337">this
-          Discourse post</link> in the tt-rss forums for more details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The following Visual Studio Code extensions were renamed to
-          keep the naming convention uniform.
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              <literal>bbenoist.Nix</literal> -&gt;
-              <literal>bbenoist.nix</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>CoenraadS.bracket-pair-colorizer</literal> -&gt;
-              <literal>coenraads.bracket-pair-colorizer</literal>
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>golang.Go</literal> -&gt;
-              <literal>golang.go</literal>
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.uptimed</literal> now uses
-          <literal>/var/lib/uptimed</literal> as its stateDirectory
-          instead of <literal>/var/spool/uptimed</literal>. Make sure to
-          move all files to the new directory.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Deprecated package aliases in <literal>emacs.pkgs.*</literal>
-          have been removed. These aliases were remnants of the old
-          Emacs package infrastructure. We now use exact upstream names
-          wherever possible.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs.neovim.runtime</literal> switched to a
-          <literal>linkFarm</literal> internally, making it impossible
-          to use wildcards in the <literal>source</literal> argument.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>openrazer</literal> and
-          <literal>openrazer-daemon</literal> packages as well as the
-          <literal>hardware.openrazer</literal> module now require users
-          to be members of the <literal>openrazer</literal> group
-          instead of <literal>plugdev</literal>. With this change, users
-          no longer need be granted the entire set of
-          <literal>plugdev</literal> group permissions, which can
-          include permissions other than those required by
-          <literal>openrazer</literal>. This is desirable from a
-          security point of view. The setting
-          <link xlink:href="options.html#opt-services.hardware.openrazer.users"><literal>harware.openrazer.users</literal></link>
-          can be used to add users to the <literal>openrazer</literal>
-          group.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The fontconfig service’s dpi option has been removed.
-          Fontconfig should use Xft settings by default so there’s no
-          need to override one value in multiple places. The user can
-          set DPI via ~/.Xresources properly, or at the system level per
-          monitor, or as a last resort at the system level with
-          <literal>services.xserver.dpi</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>yambar</literal> package has been split into
-          <literal>yambar</literal> and
-          <literal>yambar-wayland</literal>, corresponding to the xorg
-          and wayland backend respectively. Please switch to
-          <literal>yambar-wayland</literal> if you are on wayland.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.minio</literal> module gained an
-          additional option <literal>consoleAddress</literal>, that
-          configures the address and port the web UI is listening, it
-          defaults to <literal>:9001</literal>. To be able to access the
-          web UI this port needs to be opened in the firewall.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>varnish</literal> package was upgraded from 6.3.x
-          to 7.x. <literal>varnish60</literal> for the last LTS release
-          is also still available.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>kubernetes</literal> package was upgraded to
-          1.22. The <literal>kubernetes.apiserver.kubeletHttps</literal>
-          option was removed and HTTPS is always used.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The attribute <literal>linuxPackages_latest_hardened</literal>
-          was dropped because the hardened patches lag behind the
-          upstream kernel which made version bumps harder. If you want
-          to use a hardened kernel, please pin it explicitly with a
-          versioned attribute such as
-          <literal>linuxPackages_5_10_hardened</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>nomad</literal> package now defaults to a 1.1.x
-          release instead of 1.0.x
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          If <literal>exfat</literal> is included in
-          <literal>boot.supportedFilesystems</literal> and when using
-          kernel 5.7 or later, the <literal>exfatprogs</literal>
-          user-space utilities are used instead of
-          <literal>exfat</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>todoman</literal> package was upgraded from 3.9.0
-          to 4.0.0. This introduces breaking changes in the
-          <link xlink:href="https://todoman.readthedocs.io/en/stable/configure.html#configuration-file">configuration
-          file</link> format.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>datadog-agent</literal>,
-          <literal>datadog-integrations-core</literal> and
-          <literal>datadog-process-agent</literal> packages were
-          upgraded from 6.11.2 to 7.30.2, git-2018-09-18 to 7.30.1 and
-          6.11.1 to 7.30.2, respectively. As a result
-          <literal>services.datadog-agent</literal> has had breaking
-          changes to the configuration file. For details, see the
-          <link xlink:href="https://github.com/DataDog/datadog-agent/blob/main/CHANGELOG.rst">upstream
-          changelog</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>opencv2</literal> no longer includes the non-free
-          libraries by default, and consequently
-          <literal>pfstools</literal> no longer includes OpenCV support
-          by default. Both packages now support an
-          <literal>enableUnfree</literal> option to re-enable this
-          functionality.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.xserver.displayManager.defaultSession = &quot;plasma5&quot;</literal>
-          does not work anymore, instead use either
-          <literal>&quot;plasma&quot;</literal> for the Plasma X11
-          session or <literal>&quot;plasmawayland&quot;</literal> for
-          the Plasma Wayland sesison.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>boot.kernelParams</literal> now only accepts one
-          command line parameter per string. This change is aimed to
-          reduce common mistakes like <quote>param = 12</quote>, which
-          would be parsed as 3 parameters.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>nix.daemonNiceLevel</literal> and
-          <literal>nix.daemonIONiceLevel</literal> have been removed in
-          favour of the new options
-          <link xlink:href="options.html#opt-nix.daemonCPUSchedPolicy"><literal>nix.daemonCPUSchedPolicy</literal></link>,
-          <link xlink:href="options.html#opt-nix.daemonIOSchedClass"><literal>nix.daemonIOSchedClass</literal></link>
-          and
-          <link xlink:href="options.html#opt-nix.daemonIOSchedPriority"><literal>nix.daemonIOSchedPriority</literal></link>.
-          Please refer to the options documentation and the
-          <literal>sched(7)</literal> and
-          <literal>ioprio_set(2)</literal> man pages for guidance on how
-          to use them.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>coursier</literal> package’s binary was renamed
-          from <literal>coursier</literal> to <literal>cs</literal>.
-          Completions which haven’t worked for a while should now work
-          with the renamed binary. To keep using
-          <literal>coursier</literal>, you can create a shell alias.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.mosquitto</literal> module has been
-          rewritten to support multiple listeners and per-listener
-          configuration. Module configurations from previous releases
-          will no longer work and must be updated.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>fluidsynth_1</literal> attribute has been
-          removed, as this legacy version is no longer needed in
-          nixpkgs. The actively maintained 2.x series is available as
-          <literal>fluidsynth</literal> unchanged.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Nextcloud 20 (<literal>pkgs.nextcloud20</literal>) has been
-          dropped because it was EOLed by upstream in 2021-10.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>virtualisation.pathsInNixDB</literal> option was
-          renamed
-          <link xlink:href="options.html#opt-virtualisation.additionalPaths"><literal>virtualisation.additionalPaths</literal></link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.ddclient.password</literal> option was
-          removed, and replaced with
-          <literal>services.ddclient.passwordFile</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default GNAT version has been changed: The
-          <literal>gnat</literal> attribute now points to
-          <literal>gnat12</literal> instead of <literal>gnat9</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>retroArchCores</literal> has been removed. This means
-          that using <literal>nixpkgs.config.retroarch</literal> to
-          customize RetroArch cores is not supported anymore. Instead,
-          use package overrides, for example:
-          <literal>retroarch.override { cores = with libretro; [ citra snes9x ]; };</literal>.
-          Also, <literal>retroarchFull</literal> derivation is available
-          for those who want to have all RetroArch cores available.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Linux kernel for security reasons now restricts access to
-          BPF syscalls via <literal>BPF_UNPRIV_DEFAULT_OFF=y</literal>.
-          Unprivileged access can be reenabled via the
-          <literal>kernel.unprivileged_bpf_disabled</literal> sysctl
-          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>
-      <listitem>
-        <para>
-          <literal>pkgs.haskell-language-server</literal> will now by
-          default be linked dynamically to improve TemplateHaskell
-          compatibility. To mitigate the increased closure size it will
-          now by default only support our current default ghc (at the
-          moment 9.0.2). Add other ghc versions via e.g.
-          <literal>pkgs.haskell-language-server.override { supportedGhcVersions = [ &quot;90&quot; &quot;92&quot; ]; }</literal>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-21.11-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The linux kernel package infrastructure was moved out of
-          <literal>all-packages.nix</literal>, and restructured. Linux
-          related functions and attributes now live under the
-          <literal>pkgs.linuxKernel</literal> attribute set. In
-          particular the versioned <literal>linuxPackages_*</literal>
-          package sets (such as <literal>linuxPackages_5_4</literal>)
-          and kernels from <literal>pkgs</literal> were moved there and
-          now live under <literal>pkgs.linuxKernel.packages.*</literal>.
-          The unversioned ones (such as
-          <literal>linuxPackages_latest</literal>) remain untouched.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          In NixOS virtual machines (QEMU), the
-          <literal>virtualisation</literal> module has been updated with
-          new options:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-virtualisation.forwardPorts"><literal>forwardPorts</literal></link>
-              to configure IPv4 port forwarding,
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-virtualisation.sharedDirectories"><literal>sharedDirectories</literal></link>
-              to set up shared host directories,
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-virtualisation.resolution"><literal>resolution</literal></link>
-              to set the screen resolution,
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-virtualisation.useNixStoreImage"><literal>useNixStoreImage</literal></link>
-              to use a disk image for the Nix store instead of 9P.
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          In addition, the default
-          <link xlink:href="options.html#opt-virtualisation.msize"><literal>msize</literal></link>
-          parameter in 9P filesystems (including /nix/store and all
-          shared directories) has been increased to 16K for improved
-          performance.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The setting
-          <link xlink:href="options.html#opt-services.openssh.logLevel"><literal>services.openssh.logLevel</literal></link>
-          <literal>&quot;VERBOSE&quot;</literal>
-          <literal>&quot;INFO&quot;</literal>. This brings NixOS in line
-          with upstream and other Linux distributions, and reduces log
-          spam on servers due to bruteforcing botnets.
-        </para>
-        <para>
-          However, if
-          <link xlink:href="options.html#opt-services.fail2ban.enable"><literal>services.fail2ban.enable</literal></link>
-          is <literal>true</literal>, the <literal>fail2ban</literal>
-          will override the verbosity to
-          <literal>&quot;VERBOSE&quot;</literal>, so that
-          <literal>fail2ban</literal> can observe the failed login
-          attempts from the SSH logs.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.xserver.extraLayouts"><literal>services.xserver.extraLayouts</literal></link>
-          no longer cause additional rebuilds when a layout is added or
-          modified.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Sway: The terminal emulator <literal>rxvt-unicode</literal> is
-          no longer installed by default via
-          <literal>programs.sway.extraPackages</literal>. The current
-          default configuration uses <literal>alacritty</literal> (and
-          soon <literal>foot</literal>) so this is only an issue when
-          using a customized configuration and not installing
-          <literal>rxvt-unicode</literal> explicitly.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>python3</literal> now defaults to Python 3.9. Python
-          3.9 introduces many deprecation warnings, please look at the
-          <link xlink:href="https://docs.python.org/3/whatsnew/3.9.html">What’s
-          New In Python 3.9 post</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>qtile</literal> hase been updated from
-          <quote>0.16.0</quote> to <quote>0.18.0</quote>, please check
-          <link xlink:href="https://github.com/qtile/qtile/blob/master/CHANGELOG">qtile
-          changelog</link> for changes.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>claws-mail</literal> package now references the
-          new GTK+ 3 release branch, major version 4. To use the GTK+ 2
-          releases, one can install the
-          <literal>claws-mail-gtk2</literal> package.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The wordpress module provides a new interface which allows to
-          use different webservers with the new option
-          <link xlink:href="options.html#opt-services.wordpress.webserver"><literal>services.wordpress.webserver</literal></link>.
-          Currently <literal>httpd</literal>, <literal>caddy</literal>
-          and <literal>nginx</literal> are supported. The definitions of
-          wordpress sites should now be set in
-          <link xlink:href="options.html#opt-services.wordpress.sites"><literal>services.wordpress.sites</literal></link>.
-        </para>
-        <para>
-          Sites definitions that use the old interface are automatically
-          migrated in the new option. This backward compatibility will
-          be removed in 22.05.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The dokuwiki module provides a new interface which allows to
-          use different webservers with the new option
-          <link xlink:href="options.html#opt-services.dokuwiki.webserver"><literal>services.dokuwiki.webserver</literal></link>.
-          Currently <literal>caddy</literal> and
-          <literal>nginx</literal> are supported. The definitions of
-          dokuwiki sites should now be set in
-          <link xlink:href="options.html#opt-services.dokuwiki.sites"><literal>services.dokuwiki.sites</literal></link>.
-        </para>
-        <para>
-          Sites definitions that use the old interface are automatically
-          migrated in the new option. This backward compatibility will
-          be removed in 22.05.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The order of NSS (host) modules has been brought in line with
-          upstream recommendations:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              The <literal>myhostname</literal> module is placed before
-              the <literal>resolve</literal> (optional) and
-              <literal>dns</literal> entries, but after
-              <literal>file</literal> (to allow overriding via
-              <literal>/etc/hosts</literal> /
-              <literal>networking.extraHosts</literal>, and prevent ISPs
-              with catchall-DNS resolvers from hijacking
-              <literal>.localhost</literal> domains)
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The <literal>mymachines</literal> module, which provides
-              hostname resolution for local containers (registered with
-              <literal>systemd-machined</literal>) is placed to the
-              front, to make sure its mappings are preferred over other
-              resolvers.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              If systemd-networkd is enabled, the
-              <literal>resolve</literal> module is placed before
-              <literal>files</literal> and
-              <literal>myhostname</literal>, as it provides the same
-              logic internally, with caching.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The <literal>mdns(_minimal)</literal> module has been
-              updated to the new priorities.
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          If you use your own NSS host modules, make sure to update your
-          priorities according to these rules:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              NSS modules which should be queried before
-              <literal>resolved</literal> DNS resolution should use
-              mkBefore.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              NSS modules which should be queried after
-              <literal>resolved</literal>, <literal>files</literal> and
-              <literal>myhostname</literal>, but before
-              <literal>dns</literal> should use the default priority
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              NSS modules which should come after <literal>dns</literal>
-              should use mkAfter.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-networking.wireless.enable">networking.wireless</link>
-          module (based on wpa_supplicant) has been heavily reworked,
-          solving a number of issues and adding useful features:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              The automatic discovery of wireless interfaces at boot has
-              been made reliable again (issues
-              <link xlink:href="https://github.com/NixOS/nixpkgs/issues/101963">#101963</link>,
-              <link xlink:href="https://github.com/NixOS/nixpkgs/issues/23196">#23196</link>).
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              WPA3 and Fast BSS Transition (802.11r) are now enabled by
-              default for all networks.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Secrets like pre-shared keys and passwords can now be
-              handled safely, meaning without including them in a
-              world-readable file
-              (<literal>wpa_supplicant.conf</literal> under /nix/store).
-              This is achieved by storing the secrets in a secured
-              <link xlink:href="options.html#opt-networking.wireless.environmentFile">environmentFile</link>
-              and referring to them though environment variables that
-              are expanded inside the configuration.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              With multiple interfaces declared, independent
-              wpa_supplicant daemons are started, one for each interface
-              (the services are named
-              <literal>wpa_supplicant-wlan0</literal>,
-              <literal>wpa_supplicant-wlan1</literal>, etc.).
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The generated <literal>wpa_supplicant.conf</literal> file
-              is now formatted for easier reading.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              A new
-              <link xlink:href="options.html#opt-networking.wireless.scanOnLowSignal">scanOnLowSignal</link>
-              option has been added to facilitate fast roaming between
-              access points (enabled by default).
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              A new
-              <link xlink:href="options.html#opt-networking.wireless.networks._name_.authProtocols">networks.&lt;name&gt;.authProtocols</link>
-              option has been added to change the authentication
-              protocols used when connecting to a network.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-networking.wireless.iwd.enable">networking.wireless.iwd</link>
-          module has a new
-          <link xlink:href="options.html#opt-networking.wireless.iwd.settings">networking.wireless.iwd.settings</link>
-          option.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.smokeping.host">services.smokeping.host</link>
-          option was added and defaulted to
-          <literal>localhost</literal>. Before,
-          <literal>smokeping</literal> listened to all interfaces by
-          default. NixOS defaults generally aim to provide
-          non-Internet-exposed defaults for databases and internal
-          monitoring tools, see e.g.
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/100192">#100192</link>.
-          Further, the systemd service for <literal>smokeping</literal>
-          got reworked defaults for increased operational stability, see
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/144127">PR
-          #144127</link> for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.syncoid.enable">services.syncoid.enable</link>
-          module now properly drops ZFS permissions after usage. Before
-          it delegated permissions to whole pools instead of datasets
-          and didn’t clean up after execution. You can manually look
-          this up for your pools by running
-          <literal>zfs allow your-pool-name</literal> and use
-          <literal>zfs unallow syncoid your-pool-name</literal> to clean
-          this up.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Zfs: <literal>latestCompatibleLinuxPackages</literal> is now
-          exported on the zfs package. One can use
-          <literal>boot.kernelPackages = config.boot.zfs.package.latestCompatibleLinuxPackages;</literal>
-          to always track the latest compatible kernel with a given
-          version of zfs.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Nginx will use the value of
-          <literal>sslTrustedCertificate</literal> if provided for a
-          virtual host, even if <literal>enableACME</literal> is set.
-          This is useful for providers not using the same certificate to
-          sign OCSP responses and server certificates.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.formats.yaml</literal>’s
-          <literal>generate</literal> will not generate JSON anymore,
-          but instead use more of the YAML-specific syntax.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          MariaDB was upgraded from 10.5.x to 10.6.x. Please read the
-          <link xlink:href="https://mariadb.com/kb/en/changes-improvements-in-mariadb-106/">upstream
-          release notes</link> for changes and upgrade instructions.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The MariaDB C client library, also known as libmysqlclient or
-          mariadb-connector-c, was upgraded from 3.1.x to 3.2.x. While
-          this should hopefully not have any impact, this upgrade comes
-          with some changes to default behavior, so you might want to
-          review the
-          <link xlink:href="https://mariadb.com/kb/en/changes-and-improvements-in-mariadb-connector-c-32/">upstream
-          release notes</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          GNOME desktop environment now enables
-          <literal>QGnomePlatform</literal> as the Qt platform theme,
-          which should avoid crashes when opening file chooser dialogs
-          in Qt apps by using XDG desktop portal. Additionally, it will
-          make the apps fit better visually.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>rofi</literal> has been updated from
-          <quote>1.6.1</quote> to <quote>1.7.0</quote>, one important
-          thing is the removal of the old xresources based configuration
-          setup. Read more
-          <link xlink:href="https://github.com/davatorium/rofi/blob/cb12e6fc058f4a0f4f/Changelog#L1">in
-          rofi’s changelog</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          ipfs now defaults to not listening on you local network. This
-          setting was change as server providers won’t accept port
-          scanning on their private network. If you have several ipfs
-          instances running on a network you own, feel free to change
-          the setting <literal>ipfs.localDiscovery = true;</literal>.
-          localDiscovery enables different instances to discover each
-          other and share data.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lua</literal> and <literal>luajit</literal>
-          interpreters have been patched to avoid looking into /usr/lib
-          directories, thus increasing the purity of the build.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Three new options,
-          <link linkend="opt-xdg.mime.addedAssociations">xdg.mime.addedAssociations</link>,
-          <link linkend="opt-xdg.mime.defaultApplications">xdg.mime.defaultApplications</link>,
-          and
-          <link linkend="opt-xdg.mime.removedAssociations">xdg.mime.removedAssociations</link>
-          have been added to the
-          <link linkend="opt-xdg.mime.enable">xdg.mime</link> module to
-          allow the configuration of
-          <literal>/etc/xdg/mimeapps.list</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Kopia was upgraded from 0.8.x to 0.9.x. Please read the
-          <link xlink:href="https://github.com/kopia/kopia/releases/tag/v0.9.0">upstream
-          release notes</link> for changes and upgrade instructions.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>systemd.network</literal> module has gained
-          support for the FooOverUDP link type.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>networking</literal> module has a new
-          <literal>networking.fooOverUDP</literal> option to configure
-          Foo-over-UDP encapsulations.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>networking.sits</literal> now supports Foo-over-UDP
-          encapsulation.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>virtualisation.libvirtd</literal> module has been
-          refactored and updated with new options:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              <literal>virtualisation.libvirtd.qemu*</literal> options
-              (e.g.:
-              <literal>virtualisation.libvirtd.qemuRunAsRoot</literal>)
-              were moved to
-              <link xlink:href="options.html#opt-virtualisation.libvirtd.qemu"><literal>virtualisation.libvirtd.qemu</literal></link>
-              submodule,
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              software TPM1/TPM2 support (e.g.: Windows 11 guests)
-              (<link xlink:href="options.html#opt-virtualisation.libvirtd.qemu.swtpm"><literal>virtualisation.libvirtd.qemu.swtpm</literal></link>),
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              custom OVMF package (e.g.:
-              <literal>pkgs.OVMFFull</literal> with HTTP, CSM and Secure
-              Boot support)
-              (<link xlink:href="options.html#opt-virtualisation.libvirtd.qemu.ovmf.package"><literal>virtualisation.libvirtd.qemu.ovmf.package</literal></link>).
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>cawbird</literal> Twitter client now uses its own
-          API keys to count as different application than upstream
-          builds. This is done to evade application-level rate limiting.
-          While existing accounts continue to work, users may want to
-          remove and re-register their account in the client to enjoy a
-          better user experience and benefit from this change.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          A new option
-          <literal>services.prometheus.enableReload</literal> has been
-          added which can be enabled to reload the prometheus service
-          when its config file changes instead of restarting.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <literal>services.prometheus.environmentFile</literal> has
-          been removed since it was causing
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/126083">issues</link>
-          and Prometheus now has native support for secret files, i.e.
-          <literal>basic_auth.password_file</literal> and
-          <literal>authorization.credentials_file</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Dokuwiki now supports caddy! However
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              the nginx option has been removed, in the new
-              configuration, please use the
-              <literal>dokuwiki.webserver = &quot;nginx&quot;</literal>
-              instead.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The <quote>${hostname}</quote> option has been deprecated,
-              please use
-              <literal>dokuwiki.sites = [ &quot;${hostname}&quot; ]</literal>
-              instead
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="options.html#opt-services.unifi.enable">services.unifi</link>
-          module has been reworked, solving a number of issues. This
-          leads to several user facing changes:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              The <literal>services.unifi.dataDir</literal> option is
-              removed and the data is now always located under
-              <literal>/var/lib/unifi/data</literal>. This is done to
-              make better use of systemd state direcotiry and thus
-              making the service restart more reliable.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The unifi logs can now be found under:
-              <literal>/var/log/unifi</literal> instead of
-              <literal>/var/lib/unifi/logs</literal>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The unifi run directory can now be found under:
-              <literal>/run/unifi</literal> instead of
-              <literal>/var/lib/unifi/run</literal>.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>security.pam.services.&lt;name&gt;.makeHomeDir</literal>
-          now uses <literal>umask=0077</literal> instead of
-          <literal>umask=0022</literal> when creating the home
-          directory.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Loki has had another release. Some default values have been
-          changed for the configuration and some configuration options
-          have been renamed. For more details, please check
-          <link xlink:href="https://grafana.com/docs/loki/latest/upgrading/#240">the
-          upgrade guide</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>julia</literal> now refers to
-          <literal>julia-stable</literal> instead of
-          <literal>julia-lts</literal>. In practice this means it has
-          been upgraded from <literal>1.0.4</literal> to
-          <literal>1.5.4</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          RetroArch has been upgraded from version
-          <literal>1.8.5</literal> to <literal>1.9.13.2</literal>. Since
-          the previous release was quite old, if you’re having issues
-          after the upgrade, please delete your
-          <literal>$XDG_CONFIG_HOME/retroarch/retroarch.cfg</literal>
-          file.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          hydrus has been upgraded from version <literal>438</literal>
-          to <literal>463</literal>. Since upgrading between releases
-          this old is advised against, be sure to have a backup of your
-          data before upgrading. For details, see
-          <link xlink:href="https://hydrusnetwork.github.io/hydrus/help/getting_started_installing.html#big_updates">the
-          hydrus manual</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          More jdk and jre versions are now exposed via
-          <literal>java-packages.compiler</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The sets <literal>haskell.packages</literal> and
-          <literal>haskell.compiler</literal> now contain for every ghc
-          version an attribute with the minor version dropped. E.g. for
-          <literal>ghc8107</literal> there also now exists
-          <literal>ghc810</literal>. Those attributes point to the same
-          compilers and packagesets but have the advantage that e.g.
-          <literal>ghc92</literal> stays stable when we update from
-          <literal>ghc925</literal> to <literal>ghc926</literal>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</section>
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
deleted file mode 100644
index c43757a9a05..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-2205.section.xml
+++ /dev/null
@@ -1,2828 +0,0 @@
-<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/30)</title>
-  <itemizedlist spacing="compact">
-    <listitem>
-      <para>
-        Support is planned until the end of December 2022, handing over
-        to 22.11.
-      </para>
-    </listitem>
-  </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>
-          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>
-          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>
-          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>
-  </section>
-  <section xml:id="sec-release-22.05-new-services">
-    <title>New Services</title>
-    <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
-          <link linkend="opt-services.aesmd.enable">services.aesmd</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <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://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>
-        <para>
-          <link xlink:href="https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-overview.html">filebeat</link>,
-          a lightweight shipper for forwarding and centralizing log
-          data. Available as
-          <link linkend="opt-services.filebeat.enable">services.filebeat</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://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://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 linkend="opt-services.heisenbridge.enable">services.heisenbridge</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <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/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>
-        <para>
-          <link xlink:href="https://invoiceplane.com">InvoicePlane</link>,
-          web application for managing and creating invoices. Available
-          at
-          <link linkend="opt-services.invoiceplane.sites._name_.enable">services.invoiceplane</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <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. Available 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 linkend="opt-services.mtr-exporter.enable">services.mtr-exporter</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <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/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://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://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://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>
-        <para>
-          <link xlink:href="https://github.com/ThomasLeister/prosody-filer">prosody-filer</link>,
-          a server for handling XMPP HTTP Upload requests. Available at
-          <link linkend="opt-services.prosody-filer.enable">services.prosody-filer</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <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://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>
-        <para>
-          <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 linkend="opt-services.rstudio-server.enable">services.rstudio-server</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <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>
-  </section>
-  <section xml:id="sec-release-22.05-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>pkgs.ghc</literal> now refers to
-          <literal>pkgs.targetPackages.haskellPackages.ghc</literal>.
-          This <emphasis>only</emphasis> makes a difference if you are
-          cross-compiling and will ensure that
-          <literal>pkgs.ghc</literal> always runs on the host platform
-          and compiles for the target platform (similar to
-          <literal>pkgs.gcc</literal> for example).
-          <literal>haskellPackages.ghc</literal> still behaves as
-          before, running on the build platform and compiling for the
-          host platform (similar to <literal>stdenv.cc</literal>). This
-          means you don’t have to adjust your derivations if you use
-          <literal>haskellPackages.callPackage</literal>, but when using
-          <literal>pkgs.callPackage</literal> and taking
-          <literal>ghc</literal> as an input, you should now use
-          <literal>buildPackages.ghc</literal> instead to ensure cross
-          compilation keeps working (or switch to
-          <literal>haskellPackages.callPackage</literal>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>pkgs.ghc.withPackages</literal> as well as
-          <literal>haskellPackages.ghcWithPackages</literal> etc. now
-          needs be overridden directly, as opposed to overriding the
-          result of calling it. Additionally, the
-          <literal>withLLVM</literal> parameter has been renamed to
-          <literal>useLLVM</literal>. So instead of
-          <literal>(ghc.withPackages (p: [])).override { withLLVM = true; }</literal>,
-          one needs to use
-          <literal>(ghc.withPackages.override { useLLVM = true; }) (p: [])</literal>.
-        </para>
-      </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
-          org-contrib, refer to the ones in
-          <literal>pkgs.emacsPackages.elpaPackages</literal> and
-          <literal>pkgs.emacsPackages.nongnuPackages</literal> where the
-          new versions will release.
-        </para>
-      </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
-          <literal>DynamicUser</literal> mechanism to run as an
-          unprivileged dynamically-allocated user with limited
-          capabilities. The dhcpd state files are now always stored in
-          <literal>/var/lib/dhcpd{4,6}</literal> and the
-          <literal>services.dhcpd4.stateDir</literal> and
-          <literal>service.dhcpd6.stateDir</literal> options have been
-          removed. If you were depending on root privileges or
-          set{uid,gid,cap} binaries in dhcpd shell hooks, you may give
-          dhcpd more capabilities with e.g.
-          <literal>systemd.services.dhcpd6.serviceConfig.AmbientCapabilities</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>mailpile</literal> email webclient
-          (<literal>services.mailpile</literal>) has been removed due to
-          its reliance on python2.
-        </para>
-      </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.
-        </para>
-      </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
-          explicitly. Packages can restore the old behaviour by setting
-          <literal>enableParallelBuilding=false</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>pkgs.claws-mail-gtk2</literal>, representing Claws
-          Mail’s older release version three, was removed in order to
-          get rid of Python 2. Please switch to
-          <literal>claws-mail</literal>, which is Claws Mail’s latest
-          release based on GTK+3 and Python 3.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>writers.writePython2</literal> and corresponding
-          <literal>writers.writePython2Bin</literal> convenience
-          functions to create executable Python 2 scripts in the store
-          were removed in preparation of removal of the Python 2
-          interpreter. Scripts have to be converted to Python 3 for use
-          with <literal>writers.writePython3</literal> or
-          <literal>writers.writePyPy2</literal> needs to be used.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>buildGoModule</literal> was updated to use
-          <literal>go_1_17</literal>, third party derivations that
-          specify &gt;= go 1.17 in the main <literal>go.mod</literal>
-          will need to regenerate their <literal>vendorSha256</literal>
-          hash.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>gnome-passwordsafe</literal> package updated to
-          <link xlink:href="https://gitlab.gnome.org/World/secrets/-/tags/6.0">version
-          6.x</link> and renamed to <literal>gnome-secrets</literal>.
-        </para>
-      </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
-          <literal>virtualisation.docker.daemon.settings</literal>.
-        </para>
-      </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
-          <literal>services.wordpress.sites</literal> instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The backward compatibility in
-          <literal>services.dokuwiki</literal> to configure sites with
-          the old interface has been removed. Please use
-          <literal>services.dokuwiki.sites</literal> instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          opensmtpd-extras is no longer build with python2 scripting
-          support due to python2 deprecation in nixpkgs
-        </para>
-      </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
-          <link xlink:href="https://autorestic.vercel.app/migration/1.4_1.5">their
-          migration guide</link> for more details.
-        </para>
-      </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
-          is really an adapter to integrate matplotlib in ipython if it
-          is installed) does not depend on
-          <literal>pkgs.python3.pkgs.matplotlib</literal> anymore. This
-          is closer to a non-Nix install of ipython. This has the added
-          benefit to reduce the closure size of
-          <literal>ipython</literal> from ~400MB to ~160MB (including
-          ~100MB for python itself).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>documentation.man</literal> has been refactored to
-          support choosing a man implementation other than GNU’s
-          <literal>man-db</literal>. For this,
-          <literal>documentation.man.manualPages</literal> has been
-          renamed to
-          <literal>documentation.man.man-db.manualPages</literal>. If
-          you want to use the new alternative man implementation
-          <literal>mandoc</literal>, add
-          <literal>documentation.man = { enable = true; man-db.enable = false; mandoc.enable = true; }</literal>
-          to your configuration.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Normal users (with <literal>isNormalUser = true</literal>)
-          which have non-empty <literal>subUidRanges</literal> or
-          <literal>subGidRanges</literal> set no longer have additional
-          implicit ranges allocated. To enable automatic allocation back
-          set <literal>autoSubUidGidRange = true</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>idris2</literal> now requires
-          <literal>--package</literal> when using packages
-          <literal>contrib</literal> and <literal>network</literal>,
-          while previously these idris2 packages were automatically
-          loaded.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The iputils package, which is installed by default, no longer
-          provides the legacy tools <literal>tftpd</literal> and
-          <literal>traceroute6</literal>. More tools
-          (<literal>ninfod</literal>, <literal>rarpd</literal>, and
-          <literal>rdisc</literal>) are going to be removed in the next
-          release. See
-          <link xlink:href="https://github.com/iputils/iputils/releases/tag/20211215">upstream’s
-          release notes</link> for more details and available
-          replacements.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.thelounge.private</literal> was removed in
-          favor of <literal>services.thelounge.public</literal>, to
-          follow with upstream changes.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>pkgs.docbookrx</literal> was removed since it’s
-          unmaintained
-        </para>
-      </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 overridden 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
-          <literal>networking.interfaces.&lt;name&gt;.ipv6.routes</literal>
-          are no longer ignored when using networkd instead of the
-          default scripted network backend by setting
-          <literal>networking.useNetworkd</literal> to
-          <literal>true</literal>.
-        </para>
-      </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 PrismLauncher due to
-          upstream developers being hostile to 3rd party package
-          maintainers. PrismLauncher 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
-          <literal>~/.local/share/multimc</literal> to
-          <literal>~/.local/share/PrismLauncher</literal>. The main
-          config file’s path has also moved from
-          <literal>~/.local/share/multimc/multimc.cfg</literal> to
-          <literal>~/.local/share/PrismLauncher/prismlauncher.cfg</literal>.
-        </para>
-      </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>
-          implementations simplified. Providers now need to be stored
-          under
-          <literal>$out/libexec/terraform-providers/&lt;registry&gt;/&lt;owner&gt;/&lt;name&gt;/&lt;version&gt;/&lt;os&gt;_&lt;arch&gt;/terraform-provider-&lt;name&gt;_v&lt;version&gt;</literal>
-          (which mkProvider does).
-        </para>
-        <para>
-          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
-          <link xlink:href="https://github.com/numtide/nixpkgs-terraform-providers-bin">nixpkgs-terraform-providers-bin</link>
-          directly.
-        </para>
-      </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
-          match the actual source being the <quote>contrib</quote>
-          edition. <literal>pkgs.opentelemetry-collector</literal> is
-          now the actual core release of opentelemetry-collector. If you
-          use the community contributions you should change the package
-          you refer to. If you don’t need them update your commands from
-          <literal>otelcontribcol</literal> to
-          <literal>otelcorecol</literal> and enjoy a 7x smaller binary.
-        </para>
-      </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
-          have different release schedules. To maintain compatibility
-          with prior releases of Nixpkgs,
-          <literal>pkgs.noto-fonts-cjk</literal> is currently an alias
-          of <literal>pkgs.noto-fonts-cjk-sans</literal> and doesn’t
-          include serif fonts.
-        </para>
-      </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
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              The interface that allows activation scripts to restart
-              units has been streamlined. Restarting and reloading is
-              now done by a single file
-              <literal>/run/nixos/activation-restart-list</literal> that
-              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>
-              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.
-            </para>
-            <itemizedlist spacing="compact">
-              <listitem>
-                <para>
-                  <literal>RefuseManualStop</literal>,
-                  <literal>X-OnlyManualStart</literal>,
-                  <literal>X-StopOnRemoval</literal>,
-                  <literal>X-StopOnReconfiguration</literal> are only
-                  searched in the <literal>[Unit]</literal> section
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  <literal>X-ReloadIfChanged</literal>,
-                  <literal>X-RestartIfChanged</literal>,
-                  <literal>X-StopIfChanged</literal> are only searched
-                  in the <literal>[Service]</literal> section
-                </para>
-              </listitem>
-            </itemizedlist>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.bookstack.cacheDir</literal> option has
-          been removed, since the cache directory is now handled by
-          systemd.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.bookstack.extraConfig</literal> option
-          has been replaced by
-          <literal>services.bookstack.config</literal> which implements
-          a
-          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">settings-style</link>
-          configuration.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.assertMsg</literal> and
-          <literal>lib.assertOneOf</literal> no longer return
-          <literal>false</literal> if the passed condition is
-          <literal>false</literal>, <literal>throw</literal>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 <literal>assert</literal> conditions.
-        </para>
-      </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">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The option
-          <link linkend="opt-services.redis.servers">services.redis.servers</link>
-          was added to support per-application
-          <literal>redis-server</literal> which is more secure since
-          Redis databases are only mere key prefixes without any
-          configuration or ACL of their own. Backward-compatibility is
-          preserved by mapping old
-          <literal>services.redis.settings</literal> to
-          <literal>services.redis.servers.&quot;&quot;.settings</literal>,
-          but you are strongly encouraged to name each
-          <literal>redis-server</literal> instance after the application
-          using it, instead of keeping that nameless one. Except for the
-          nameless
-          <literal>services.redis.servers.&quot;&quot;</literal> still
-          accessible at <literal>127.0.0.1:6379</literal>, and to the
-          members of the Unix group <literal>redis</literal> through the
-          Unix socket <literal>/run/redis/redis.sock</literal>, all
-          other <literal>services.redis.servers.${serverName}</literal>
-          are only accessible by default to the members of the Unix
-          group <literal>redis-${serverName}</literal> through the Unix
-          socket <literal>/run/redis-${serverName}/redis.sock</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <link linkend="opt-virtualisation.vmVariant">virtualisation.vmVariant</link>
-          was added to allow users to make changes to the
-          <literal>nixos-rebuild build-vm</literal> configuration that
-          do not apply to their normal system.
-        </para>
-        <para>
-          The <literal>config.system.build.vm</literal> attribute now
-          always exists and defaults to the value from
-          <literal>vmVariant</literal>. Configurations that import the
-          <literal>virtualisation/qemu-vm.nix</literal> module
-          themselves will override this value, such that
-          <literal>vmVariant</literal> is not used.
-        </para>
-        <para>
-          Similarly
-          <link linkend="opt-virtualisation.vmVariantWithBootLoader">virtualisation.vmVariantWithBootloader</link>
-          was added.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The configuration portion of the <literal>nix-daemon</literal>
-          module has been reworked and exposed as
-          <link xlink:href="options.html#opt-nix-settings">nix.settings</link>:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              Legacy options have been mapped to the corresponding
-              options under under
-              <link xlink:href="options.html#opt-nix.settings">nix.settings</link>
-              and will be deprecated when NixOS 21.11 reaches end of
-              life.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <link xlink:href="options.html#opt-nix.buildMachines.publicHostKey">nix.buildMachines.publicHostKey</link>
-              has been added.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </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
-          <literal>writers.writePyPy2Bin</literal>/<literal>writers.writePyPy3Bin</literal>
-          convenience functions to create executable Python 2/3 scripts
-          using the PyPy interpreter were added.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          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>
-          Added the <literal>keter</literal> NixOS module. Keter reverse
-          proxies requests to your loaded application based on virtual
-          hostnames.
-        </para>
-      </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>
-          (for example via
-          <literal>environment.sessionVariables.NIXOS_OZONE_WL = &quot;1&quot;</literal>).
-          This is not enabled by default because Ozone Wayland is still
-          under heavy development and behavior is not always flawless.
-          Furthermore, not all Electron apps use the latest Electron
-          versions.
-        </para>
-      </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
-          place upstream. A combined <literal>influxdb2</literal>
-          package is still provided in this release for backwards
-          compatibilty, but will be removed at a later date.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>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
-          using this default will print a warning when rebuilt.
-        </para>
-      </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.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Removing domains from
-          <literal>security.acme.certs._name_.extraDomainNames</literal>
-          will now correctly remove those domains during rebuild/renew.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          MariaDB is now offered in several versions, not just the
-          newest one. So if you have a need for running MariaDB 10.4 for
-          example, you can now just set
-          <literal>services.mysql.package = pkgs.mariadb_104;</literal>.
-          In general, it is recommended to run the newest version, to
-          get the newest features, while sticking with an LTS version
-          will most likely provide a more stable experience. Sometimes
-          software is also incompatible with the newest version of
-          MariaDB.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <link linkend="opt-programs.ssh.enableAskPassword">programs.ssh.enableAskPassword</link>
-          was added, decoupling the setting of
-          <literal>SSH_ASKPASS</literal> from
-          <literal>services.xserver.enable</literal>. This allows easy
-          usage in non-X11 environments, e.g. Wayland.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link linkend="opt-programs.ssh.knownHosts">programs.ssh.knownHosts</link>
-          has gained an <literal>extraHostNames</literal> option to
-          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>
-        <para>
-          The <literal>services.stubby</literal> module was converted to
-          a
-          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">settings-style</link>
-          configuration.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <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.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          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>
-        <para>
-          <literal>nixos-generate-config</literal> now puts the dhcp
-          configuration in <literal>hardware-configuration.nix</literal>
-          instead of <literal>configuration.nix</literal>.
-        </para>
-      </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
-          <literal>fetchSubmodules</literal> is set to
-          <literal>true</literal>.
-        </para>
-      </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
-          <literal>pkgs.theLoungePlugins.plugins</literal> and
-          <literal>pkgs.theLoungePlugins.themes</literal>.
-        </para>
-      </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
-          0042</link> configuration.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default value for
-          <literal>programs.spacefm.settings.graphical_su</literal> got
-          unset. It previously pointed to <literal>gksu</literal> which
-          has been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          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>
-        <para>
-          <literal>services.mattermost.plugins</literal> has been added
-          to allow the declarative installation of Mattermost plugins.
-          Plugins are automatically repackaged using autoPatchelf.
-        </para>
-      </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:
-          <literal>services.logrotate.paths</literal> and
-          <literal>services.logrotate.extraConfig</literal> 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>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              The RPC protocol version was bumped; all zrepl daemons in
-              a setup must be updated and restarted before replication
-              can resume.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              A bug involving encrypt-on-receive has been fixed. Read
-              the
-              <link xlink:href="https://zrepl.github.io/configuration/sendrecvoptions.html#job-recv-options-placeholder">zrepl
-              documentation</link> and check the output of
-              <literal>zfs get -r encryption,zrepl:placeholder PATH_TO_ROOTFS</literal>
-              on the receiver.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </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
-          <literal>services.openssh.kbdInteractiveAuthentication</literal>.
-          Reason is that the old name has been deprecated upstream.
-          Using the old option name will still work, but produce a
-          warning.
-        </para>
-      </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-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
-          <link xlink:href="https://modemmanager.org/docs/modemmanager/fcc-unlock/">the
-          docs</link> for more details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>programs.tmux</literal> has a new option
-          <literal>plugins</literal> that accepts a list of packages
-          from the <literal>tmuxPlugins</literal> group. The specified
-          packages are added to the system and loaded by
-          <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
deleted file mode 100644
index f7168d5ea17..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
+++ /dev/null
@@ -1,1841 +0,0 @@
-<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/30)</title>
-  <para>
-    The NixOS release team is happy to announce a new version of NixOS
-    22.11. NixOS is a Linux distribution, whose set of packages can also
-    be used on other Linux systems and macOS.
-  </para>
-  <para>
-    This release is supported until the end of June 2023, handing over
-    to NixOS 23.05.
-  </para>
-  <para>
-    To upgrade to the latest release follow the
-    <link linkend="sec-upgrading">upgrade chapter</link>.
-  </para>
-  <section xml:id="sec-release-22.11-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      includes the following highlights:
-    </para>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Software that uses the <literal>crypt</literal> password
-          hashing API is now using the implementation provided by
-          <link xlink:href="https://github.com/besser82/libxcrypt"><literal>libxcrypt</literal></link>
-          instead of glibc’s, which enables support for more secure
-          algorithms.
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              Support for algorithms that <literal>libxcrypt</literal>
-              <link xlink:href="https://github.com/besser82/libxcrypt/blob/v4.4.28/lib/hashes.conf#L41">does
-              not consider strong</link> are
-              <emphasis role="strong">deprecated</emphasis> as of this
-              release, and will be removed in NixOS 23.05.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              This includes system login passwords. Given this, we
-              <emphasis role="strong">strongly encourage</emphasis> all
-              users to update their system passwords, as you will be
-              unable to login if password hashes are not migrated by the
-              time their support is removed.
-            </para>
-            <itemizedlist spacing="compact">
-              <listitem>
-                <para>
-                  When using
-                  <literal>users.users.&lt;name&gt;.hashedPassword</literal>
-                  to configure user passwords, run
-                  <literal>mkpasswd</literal>, and use the yescrypt hash
-                  that is provided as the new value.
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  On the other hand, for interactively configured user
-                  passwords, simply re-set the passwords for all users
-                  with <literal>passwd</literal>.
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  This release introduces warnings for the use of
-                  deprecated hash algorithms for both methods of
-                  configuring passwords. To make sure you migrated
-                  correctly, run
-                  <literal>nixos-rebuild switch</literal>.
-                </para>
-              </listitem>
-            </itemizedlist>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The NixOS documentation is now generated from markdown. While
-          docbook is still part of the documentation build process, it’s
-          a big step towards the full migration.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>aarch64-linux</literal> is now included in the
-          <literal>nixos-22.11</literal> and
-          <literal>nixos-22.11-small</literal> channels. This means that
-          when those channel update, both
-          <literal>x86_64-linux</literal> and
-          <literal>aarch64-linux</literal> will be available in the
-          binary cache.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>aarch64-linux</literal> ISOs are now available on the
-          <link xlink:href="https://nixos.org/download.html">downloads
-          page</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>nsncd</literal> is now available as a replacement of
-          <literal>nscd</literal>.
-        </para>
-        <para>
-          <literal>nscd</literal> is responsible for resolving
-          hostnames, users and more in NixOS and has been a long
-          standing source of bugs, such as sporadic network freezes.
-        </para>
-        <para>
-          More context in this
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/135888">issue</link>.
-        </para>
-        <para>
-          Help us test the new implementation by setting
-          <literal>services.nscd.enableNsncd</literal> to
-          <literal>true</literal>.
-        </para>
-        <para>
-          We plan to use <literal>nsncd</literal> by default in NixOS
-          23.05.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Linode cloud images are now supported by importing
-          <literal>${modulesPath}/virtualisation/linode-image.nix</literal>
-          and accessing <literal>system.build.linodeImage</literal> on
-          the output.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>hardware.nvidia</literal> has a new option,
-          <literal>hardware.nvidia.open</literal>, that can be used to
-          enable the usage of NVIDIA’s open-source 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/">the
-          release announcement</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>emacs</literal> package now makes use of native
-          compilation which means:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              Emacs packages from Nixpkgs, builtin or not, will do
-              native compilation ahead of time so you can enjoy the
-              benefit of native compilation without compiling them on
-              you machine;
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Emacs packages from somewhere else, e.g.
-              <literal>package-install</literal>, will perform
-              asynchronously deferred native compilation. If you do not
-              want this, maybe to avoid CPU consumption for compilation,
-              you can use
-              <literal>(setq native-comp-deferred-compilation nil)</literal>
-              to disable it while still benefiting from native
-              compilation for packages from Nixpkgs.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-22.11-internal">
-    <title>Internal changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Haskell <literal>ghcWithPackages</literal> is now up to 15
-          times faster to evaluate, thanks to changing
-          <literal>lib.closePropagation</literal> from a quadratic to
-          linear complexity. Please see backward incompatibilities notes
-          below.
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/194391">https://github.com/NixOS/nixpkgs/pull/194391</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          For cross-compilation targets that can also run on the
-          building machine, we now run tests. This, for example, is the
-          case for the <literal>pkgsStatic</literal> and
-          <literal>pkgsLLVM</literal> package sets or i686 packages on
-          <literal>x86_64</literal> machines.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          To simplify cross-compilation in NixOS, this release
-          introduces the <literal>nixpkgs.hostPlatform</literal> and
-          <literal>nixpkgs.buildPlatform</literal> options. 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>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-22.11-version-updates">
-    <title>Notable version updates</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Nix has been upgraded from v2.8.1 to v2.11.0. For more
-          information, please see the release notes for
-          <link xlink:href="https://nixos.org/manual/nix/stable/release-notes/rl-2.9.html">2.9</link>,
-          <link xlink:href="https://nixos.org/manual/nix/stable/release-notes/rl-2.10.html">2.10</link>
-          and
-          <link xlink:href="https://nixos.org/manual/nix/stable/release-notes/rl-2.11.html">2.11</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          OpenSSL now defaults to OpenSSL 3, updated from 1.1.1.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          GNOME has been upgraded to version 43. Please see the
-          <link xlink:href="https://release.gnome.org/43/">release
-          notes</link> for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          KDE Plasma has been upgraded from v5.24 to v5.26. Please see
-          the release notes for
-          <link xlink:href="https://kde.org/announcements/plasma/5/5.25.0/">v5.25</link>
-          and
-          <link xlink:href="https://kde.org/announcements/plasma/5/5.26.0/">v5.26</link>
-          for more details on the included changes.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Cinnamon has been updated to 5.4, and the Cinnamon module now
-          defaults to Blueman as the Bluetooth manager and slick-greeter
-          as the LightDM greeter, to match upstream.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PHP now defaults to PHP 8.1, updated from 8.0.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Perl has been updated to 5.36, and its core module
-          <literal>HTTP::Tiny</literal> was patched to verify SSL/TLS
-          certificates by default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Python now defaults to 3.10, updated from 3.9.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-22.11-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          Nixpkgs now requires Nix 2.3 or newer.
-        </para>
-      </listitem>
-      <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
-          takes the kernel / syscall interface into account.
-          <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 hyphen arguments was dropped.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>i18n.supportedLocales</literal> is now only generated
-          with the locales set in <literal>i18n.defaultLocale</literal>
-          and <literal>i18n.extraLocaleSettings</literal>.
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              This reduces the final system closure size by up to 200MB.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              If you require all locales installed, set the option to
-              <literal>[ &quot;all&quot; ]</literal>.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Deprecated settings <literal>logrotate.paths</literal> and
-          <literal>logrotate.extraConfig</literal> have been removed.
-          Please convert any uses to
-          <link linkend="opt-services.logrotate.settings">services.logrotate.settings</link>
-          instead.
-        </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>
-          The <literal>fetchgit</literal> fetcher now uses
-          <link xlink:href="https://www.git-scm.com/docs/git-sparse-checkout/2.37.0#_internalscone_mode_handling">cone
-          mode</link> by default for sparse checkouts.
-          <link xlink:href="https://www.git-scm.com/docs/git-sparse-checkout/2.37.0#_internalsnon_cone_problems">Non-cone
-          mode</link> can be enabled by passing
-          <literal>nonConeMode = true</literal>, but note that non-cone
-          mode is deprecated and this option may be removed alongside a
-          future Git update without notice.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>fetchgit</literal> fetcher supports sparse
-          checkouts via the <literal>sparseCheckout</literal> option.
-          This used to accept a multi-line string with
-          directories/patterns to check out, but now requires a list of
-          strings.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>openssh</literal> was updated to version 9.1,
-          disabling the generation of DSA keys when using
-          <literal>ssh-keygen -A</literal> as they are insecure. Also,
-          <literal>SetEnv</literal> directives in
-          <literal>ssh_config</literal> and
-          <literal>sshd_config</literal> are now first-match-wins.
-        </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>
-          <literal>services.hbase</literal> has been renamed to
-          <literal>services.hbase-standalone</literal>. For production
-          HBase clusters, use <literal>services.hadoop.hbase</literal>
-          instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>p4</literal> package now only includes the
-          open-source Perforce Helix Core command-line client and APIs.
-          It no longer installs the unfree Helix Core Server binaries
-          <literal>p4d</literal>, <literal>p4broker</literal>, and
-          <literal>p4p</literal>. To install the Helix Core Server
-          binaries, use the <literal>p4d</literal> package instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The OpenSSL extension for the PHP interpreter used by
-          Nextcloud is built against OpenSSL 1.1 if
-          <xref linkend="opt-system.stateVersion" /> is below
-          <literal>22.11</literal>. This is to make sure that people
-          using
-          <link xlink:href="https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html">server-side
-          encryption</link> don’t lose access to their files.
-        </para>
-        <para>
-          In any other case, it’s safe to use OpenSSL 3 for PHP’s
-          OpenSSL extension. This can be done by setting
-          <xref linkend="opt-services.nextcloud.enableBrokenCiphersForSSE" />
-          to <literal>false</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>coq</literal> package and versioned variants
-          starting at <literal>coq_8_14</literal> no longer include
-          CoqIDE, which is now available through
-          <literal>coqPackages.coqide</literal>. It is still possible to
-          get CoqIDE as part of the <literal>coq</literal> package by
-          overriding the <literal>buildIde</literal> argument of the
-          derivation.
-        </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>
-          The ipfs package and module were renamed to kubo. The kubo
-          module now uses an RFC42-style <literal>settings</literal>
-          option instead of <literal>extraConfig</literal> and the
-          <literal>gatewayAddress</literal>,
-          <literal>apiAddress</literal> and
-          <literal>swarmAddress</literal> options were renamed. Using
-          the old names will print a warning but still work.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>pkgs.cosign</literal> does not provide the
-          <literal>cosigned</literal> binary anymore. The
-          <literal>sget</literal> binary has been moved into its own
-          package.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Emacs now uses the Lucid toolkit by default instead of GTK
-          because of stability and compatibility issues. Users who still
-          wish to remain using GTK can do so by using
-          <literal>emacs-gtk</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>kanidm</literal> has been updated to 1.1.0-alpha.10
-          and now requires a TLS certificate and key. It will always
-          start <literal>https</literal> and-–-if enabled-–-an LDAPS
-          server and no HTTP and LDAP server anymore.
-        </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>
-          ppd files in <literal>pkgs.cups-drv-rastertosag-gdi</literal>
-          are now gzipped. If you refer to such a ppd file with its path
-          (e.g. via
-          <link xlink:href="options.html#opt-hardware.printers.ensurePrinters">hardware.printers.ensurePrinters</link>)
-          you will need to append <literal>.gz</literal> to the path.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          xow package removed along with the
-          <literal>hardware.xow</literal> module, due to the project
-          being deprecated in favor of <literal>xone</literal>, which is
-          available via the <literal>hardware.xone</literal> module.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          dd-agent package removed along with the
-          <literal>services.dd-agent</literal> module, due to the
-          project being deprecated in favor of
-          <literal>datadog-agent</literal>, which is available via the
-          <literal>services.datadog-agent</literal> module.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>teleport</literal> has been upgraded to major version
-          10. Please see upstream
-          <link xlink:href="https://goteleport.com/docs/ver/10.0/management/operations/upgrading/">upgrade
-          instructions</link> and
-          <link xlink:href="https://goteleport.com/docs/ver/10.0/changelog/#1000">release
-          notes</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>lib.closePropagation</literal> now needs that all
-          gathered sets have an <literal>outPath</literal> attribute.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          lemmy module option
-          <literal>services.lemmy.settings.database.createLocally</literal>
-          moved to
-          <literal>services.lemmy.database.createLocally</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          virtlyst package and <literal>services.virtlyst</literal>
-          module removed, due to lack of maintainers.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>nix.checkConfig</literal> option now fully
-          disables the config check. The new
-          <literal>nix.checkAllErrors</literal> option behaves like
-          <literal>nix.checkConfig</literal> previously did.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>generateOptparseApplicativeCompletions</literal> and
-          <literal>generateOptparseApplicativeCompletion</literal> from
-          <literal>haskell.lib.compose</literal> (and
-          <literal>haskell.lib</literal>) have been deprecated in favor
-          of <literal>generateOptparseApplicativeCompletions</literal>
-          (plural!) as provided by the haskell package sets (so
-          <literal>haskellPackages.generateOptparseApplicativeCompletions</literal>
-          etc.). The latter allows for cross-compilation (by
-          automatically disabling generation of completion in the cross
-          case). For it to work properly you need to make sure that the
-          function comes from the same context as the package you are
-          trying to override, i.e. always use the same package set as
-          your package is coming from or – even better – use
-          <literal>self.generateOptparseApplicativeCompletions</literal>
-          if you are overriding a haskell package set. The old functions
-          are retained for backwards compatibility, but yield are
-          warning.
-        </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>trace</literal> binary from
-          <literal>perf-linux</literal> package has been removed, due to
-          being a duplicate of the <literal>perf</literal> binary.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>aws</literal> package has been removed due to
-          being abandoned by the upstream. It is recommended to use
-          <literal>awscli</literal> or <literal>awscli2</literal>
-          instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link xlink:href="https://ce-programming.github.io/CEmu">CEmu
-          TI-84 Plus CE emulator</link> package has been renamed to
-          <literal>cemu-ti</literal>. The
-          <link xlink:href="https://cemu.info">Cemu Wii U
-          emulator</link> is now packaged as <literal>cemu</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>systemd-networkd</literal> v250 deprecated, renamed,
-          and moved some sections and settings which leads to the
-          following breaking module changes:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              <literal>systemd.network.networks.&lt;name&gt;.dhcpV6PrefixDelegationConfig</literal>
-              is renamed to
-              <literal>systemd.network.networks.&lt;name&gt;.dhcpPrefixDelegationConfig</literal>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>systemd.network.networks.&lt;name&gt;.dhcpV6Config</literal>
-              no longer accepts the
-              <literal>ForceDHCPv6PDOtherInformation=</literal> setting.
-              Please use the <literal>WithoutRA=</literal> and
-              <literal>UseDelegatedPrefix=</literal> settings in your
-              <literal>systemd.network.networks.&lt;name&gt;.dhcpV6Config</literal>
-              and the <literal>DHCPv6Client=</literal> setting in your
-              <literal>systemd.network.networks.&lt;name&gt;.ipv6AcceptRAConfig</literal>
-              to control when the DHCPv6 client is started and how the
-              delegated prefixes are handled by the DHCPv6 client.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>systemd.network.networks.&lt;name&gt;.networkConfig</literal>
-              no longer accepts the <literal>IPv6Token=</literal>
-              setting. Use the <literal>Token=</literal> setting in your
-              <literal>systemd.network.networks.&lt;name&gt;.ipv6AcceptRAConfig</literal>
-              instead. The
-              <literal>systemd.network.networks.&lt;name&gt;.ipv6Prefixes.*.ipv6PrefixConfig</literal>
-              now also accepts the <literal>Token=</literal> setting.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>arangodb</literal> versions 3.3, 3.4, and 3.5 have
-          been removed because they are at EOL upstream. The default is
-          now 3.10.0. Support for aarch64-linux has been removed since
-          the target cannot be built reproducibly. By default
-          <literal>arangodb</literal> is now built for the
-          <literal>haswell</literal> architecture. If you wish to build
-          for a different architecture, you may override the
-          <literal>targetArchitecture</literal> argument with a value
-          from
-          <link xlink:href="https://github.com/arangodb/arangodb/blob/207ec6937e41a46e10aea34953879341f0606841/cmake/OptimizeForArchitecture.cmake#L594">this
-          list supported upstream</link>. Some architecture specific
-          optimizations are also conditionally enabled. You may alter
-          this behavior by overriding the
-          <literal>asmOptimizations</literal> parameter. You may also
-          add additional architecture support by adding more
-          <literal>-DHAS_XYZ</literal> flags to
-          <literal>cmakeFlags</literal> via
-          <literal>overrideAttrs</literal>.
-        </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>
-          The <literal>paperless</literal> module now defaults
-          <literal>PAPERLESS_TIME_ZONE</literal> to your configured
-          system timezone.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The top-level <literal>termonad-with-packages</literal> alias
-          for <literal>termonad</literal> has been removed.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Linux 4.9 has been removed because it will reach its end of
-          life within the lifespan of 22.11.
-        </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>
-          Neovim can not be configured with plug anymore (still works
-          for vim).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>adguardhome</literal> module no longer uses
-          <literal>host</literal> and <literal>port</literal> options,
-          use <literal>settings.bind_host</literal> and
-          <literal>settings.bind_port</literal> instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default <literal>kops</literal> version is now 1.25.1 and
-          support for 1.22 and older has been dropped.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>zrepl</literal> package has been updated from
-          0.5.0 to 0.6.0. See the
-          <link xlink:href="https://zrepl.github.io/changelog.html">changelog</link>
-          for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>k3s</literal> no longer supports Docker as runtime
-          due to upstream dropping support.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>cassandra_2_1</literal> and
-          <literal>cassandra_2_2</literal> have been removed. Please
-          update to <literal>cassandra_3_11</literal> or
-          <literal>cassandra_3_0</literal>. See the
-          <link xlink:href="https://github.com/apache/cassandra/blob/cassandra-3.11.14/NEWS.txt">changelog</link>
-          for more information about the upgrade process.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>mysql57</literal> has been removed. Please update to
-          <literal>mysql80</literal> or <literal>mariadb</literal>. See
-          the
-          <link xlink:href="https://mariadb.com/kb/en/upgrading-from-mysql-to-mariadb/">upgrade
-          guide</link> for more information.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Consequently, <literal>cqrlog</literal> and
-          <literal>amorok</literal> now use <literal>mariadb</literal>
-          instead of <literal>mysql57</literal> for their embedded
-          databases. Running <literal>mysql_upgrade</literal> may be
-          neccesary.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>k3s</literal> supports <literal>clusterInit</literal>
-          option, and it is enabled by default, for servers.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>percona-server56</literal> has been removed. Please
-          migrate to <literal>mysql</literal> or
-          <literal>mariadb</literal> if possible.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>obs-studio</literal> hase been updated to version 28.
-          If you have packaged custom plugins, check if they are
-          compatible. <literal>obs-websocket</literal> has been
-          integrated into <literal>obs-studio</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>signald</literal> has been bumped to
-          <literal>0.23.0</literal>. For the upgrade, a migration
-          process is necessary. It can be done by running a command like
-          this before starting <literal>signald.service</literal>:
-        </para>
-        <programlisting>
-signald -d /var/lib/signald/db \
-  --database sqlite:/var/lib/signald/db \
-  --migrate-data
-</programlisting>
-        <para>
-          For further information, please read the upstream changelogs.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>stylua</literal> no longer accepts
-          <literal>lua52Support</literal> and
-          <literal>luauSupport</literal> overrides. Use
-          <literal>features</literal> instead, which defaults to
-          <literal>[ &quot;lua54&quot; &quot;luau&quot; ]</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>ocamlPackages.ocaml_extlib</literal> has been renamed
-          to <literal>ocamlPackages.extlib</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>pkgs.fetchNextcloudApp</literal> has been rewritten
-          to circumvent impurities in e.g. tarballs from GitHub and to
-          make it easier to apply patches. This means that your hashes
-          are out-of-date and the (previously required) attributes
-          <literal>name</literal> and <literal>version</literal> are no
-          longer accepted.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Syncthing service now only allows absolute paths—starting
-          with <literal>/</literal> or <literal>~/</literal>—for
-          <literal>services.syncthing.folders.&lt;name&gt;.path</literal>.
-          In a future release other paths will be allowed again and
-          interpreted relative to
-          <literal>services.syncthing.dataDir</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.github-runner</literal> and
-          <literal>services.github-runners.&lt;name&gt;</literal> gained
-          the option <literal>serviceOverrides</literal> which allows
-          overriding the systemd <literal>serviceConfig</literal>. If
-          you have been overriding the systemd service configuration
-          (i.e., by defining
-          <literal>systemd.services.github-runner.serviceConfig</literal>),
-          you have to use the <literal>serviceOverrides</literal> option
-          now. Example:
-        </para>
-        <programlisting>
-services.github-runner.serviceOverrides.SupplementaryGroups = [
-  &quot;docker&quot;
-];
-</programlisting>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-22.11-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          PHP is now built in <literal>NTS</literal> (Non-Thread Safe)
-          mode by default.
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              For Apache and <literal>mod_php</literal> usage, we enable
-              <literal>ZTS</literal> (Zend Thread Safe) mode. This has
-              been a common practice for a long time in other
-              distributions.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>firefox</literal>, <literal>thunderbird</literal> and
-          <literal>librewolf</literal> now come with Wayland support by
-          default. The <literal>firefox-wayland</literal>,
-          <literal>firefox-esr-wayland</literal>,
-          <literal>thunderbird-wayland</literal> and
-          <literal>librewolf-wayland</literal> attributes are obsolete
-          and have been aliased to their generic attribute.
-        </para>
-      </listitem>
-      <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>
-          Configuring multiple GitHub runners is now possible through
-          <literal>services.github-runners.&lt;name&gt;</literal>. The
-          options under <literal>services.github-runner</literal>
-          remain, to configure a single runner.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>github-runner</literal> gained support for ephemeral
-          runners and registrations using a personal access token (PAT)
-          instead of a registration token. See
-          <literal>services.github-runner.ephemeral</literal> and
-          <literal>services.github-runner.tokenFile</literal> for
-          details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          A new module was added to provide hardware support 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>
-          ZFS module will no longer allow hibernation by default.
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              This is a safety measure to prevent data loss cases like
-              the ones described at
-              <link xlink:href="https://github.com/openzfs/zfs/issues/260">OpenZFS/260</link>
-              and
-              <link xlink:href="https://github.com/openzfs/zfs/issues/12842">OpenZFS/12842</link>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Use the <literal>boot.zfs.allowHibernation</literal>
-              option to configure this behaviour.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          Mastodon now automatically removes remote media attachments
-          older than 30 days. This is configurable through
-          <literal>services.mastodon.mediaAutoRemove</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>
-          Neo4j was updated from version 3 to version 4. See upstream’s
-          <link xlink:href="https://neo4j.com/docs/upgrade-migration-guide/current/">migration
-          guide</link> for information on how to migrate your instance.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>networking.wireguard</literal> module now can set
-          the mtu on interfaces and tag its packets with an fwmark.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option <literal>overrideStrategy</literal> was added to
-          the different systemd unit options
-          (<literal>systemd.services.&lt;name&gt;</literal>,
-          <literal>systemd.sockets.&lt;name&gt;</literal>, …) to allow
-          enforcing the creation of a dropin file, rather than the main
-          unit file, by setting it to <literal>asDropin</literal>. This
-          is useful in cases where the existence of the main unit file
-          is not known to Nix at evaluation time, for example when the
-          main unit file is provided by adding a package to
-          <literal>systemd.packages</literal>. See the fix proposed in
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/135557#issuecomment-1295392470">NixOS’s
-          systemd abstraction doesn’t work with systemd template
-          units</link> for an example.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>polymc</literal> package has been removed due to
-          a rogue maintainer. It has been replaced by
-          <literal>prismlauncher</literal>, a fork by the rest of the
-          maintainers. For more details, see
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/196624">the
-          PR that made this change</link> and
-          <link xlink:href="https://github.com/NixOS/nixpkgs/issues/196460">the
-          issue detailing the vulnerability</link>. Users with existing
-          installations should rename
-          <literal>~/.local/share/polymc</literal> to
-          <literal>~/.local/share/PrismLauncher</literal>. The main
-          config file’s path has also moved from
-          <literal>~/.local/share/polymc/polymc.cfg</literal> to
-          <literal>~/.local/share/PrismLauncher/prismlauncher.cfg</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>bloat</literal> package has been updated from
-          unstable-2022-03-31 to unstable-2022-10-25, which brings a
-          breaking change. See
-          <link xlink:href="https://git.freesoftwareextremist.com/bloat/commit/?id=887ed241d64ba5db3fd3d87194fb5595e5ad7d73">this
-          upstream commit message</link> for details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Synapse’s systemd unit has been hardened.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The module <literal>services.grafana</literal> was refactored
-          to be compliant with
-          <link xlink:href="https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md">RFC
-          0042</link>. To be precise, this means that the following
-          things have changed:
-        </para>
-        <itemizedlist>
-          <listitem>
-            <para>
-              The newly introduced option
-              <xref linkend="opt-services.grafana.settings" /> is an
-              attribute-set that will be converted into Grafana’s INI
-              format. This means that the configuration from
-              <link xlink:href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/">Grafana’s
-              configuration reference</link> can be directly written as
-              attribute-set in Nix within this option.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              The option
-              <literal>services.grafana.extraOptions</literal> has been
-              removed. This option was an association of environment
-              variables for Grafana. If you had an expression like
-            </para>
-            <programlisting language="bash">
-{
-  services.grafana.extraOptions.SECURITY_ADMIN_USER = &quot;foobar&quot;;
-}
-</programlisting>
-            <para>
-              your Grafana instance was running with
-              <literal>GF_SECURITY_ADMIN_USER=foobar</literal> in its
-              environment.
-            </para>
-            <para>
-              For the migration, it is recommended to turn it into the
-              INI format, i.e. to declare
-            </para>
-            <programlisting language="bash">
-{
-  services.grafana.settings.security.admin_user = &quot;foobar&quot;;
-}
-</programlisting>
-            <para>
-              instead.
-            </para>
-            <para>
-              The keys in
-              <literal>services.grafana.extraOptions</literal> have the
-              format
-              <literal>&lt;INI section name&gt;_&lt;Key Name&gt;</literal>.
-              Further details are outlined in the
-              <link xlink:href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#override-configuration-with-environment-variables">configuration
-              reference</link>.
-            </para>
-            <para>
-              Alternatively you can also set all your values from
-              <literal>extraOptions</literal> to
-              <literal>systemd.services.grafana.environment</literal>,
-              make sure you don’t forget to add the
-              <literal>GF_</literal> prefix though!
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Previously, the options
-              <link linkend="opt-services.grafana.provision.datasources">services.grafana.provision.datasources</link>
-              and
-              <link linkend="opt-services.grafana.provision.dashboards">services.grafana.provision.dashboards</link>
-              expected lists of datasources or dashboards for the
-              <link xlink:href="https://grafana.com/docs/grafana/latest/administration/provisioning/">declarative
-              provisioning</link>.
-            </para>
-            <para>
-              To declare lists of
-            </para>
-            <itemizedlist spacing="compact">
-              <listitem>
-                <para>
-                  <emphasis role="strong">datasources</emphasis>, please
-                  rename your declarations to
-                  <link linkend="opt-services.grafana.provision.datasources.settings.datasources">services.grafana.provision.datasources.settings.datasources</link>.
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  <emphasis role="strong">dashboards</emphasis>, please
-                  rename your declarations to
-                  <link linkend="opt-services.grafana.provision.dashboards.settings.providers">services.grafana.provision.dashboards.settings.providers</link>.
-                </para>
-              </listitem>
-            </itemizedlist>
-            <para>
-              This change was made to support more features for that:
-            </para>
-            <itemizedlist>
-              <listitem>
-                <para>
-                  It’s possible to declare the
-                  <literal>apiVersion</literal> of your dashboards and
-                  datasources by
-                  <link linkend="opt-services.grafana.provision.datasources.settings.apiVersion">services.grafana.provision.datasources.settings.apiVersion</link>
-                  (or
-                  <link linkend="opt-services.grafana.provision.dashboards.settings.apiVersion">services.grafana.provision.dashboards.settings.apiVersion</link>).
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  Instead of declaring datasources and dashboards in
-                  pure Nix, it’s also possible to specify configuration
-                  files (or directories) with YAML instead using
-                  <link linkend="opt-services.grafana.provision.datasources.path">services.grafana.provision.datasources.path</link>
-                  (or
-                  <link linkend="opt-services.grafana.provision.dashboards.path">services.grafana.provision.dashboards.path</link>.
-                  This is useful when having provisioning files from
-                  non-NixOS Grafana instances that you also want to
-                  deploy to NixOS.
-                </para>
-                <para>
-                  <emphasis role="strong">Note:</emphasis> secrets from
-                  these files will be leaked into the store unless you
-                  use a
-                  <link xlink:href="https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#file-provider"><emphasis role="strong">file</emphasis>-provider
-                  or env-var</link> for secrets!
-                </para>
-              </listitem>
-              <listitem>
-                <para>
-                  <link linkend="opt-services.grafana.provision.notifiers">services.grafana.provision.notifiers</link>
-                  is not affected by this change because this feature is
-                  deprecated by Grafana and will probably be removed in
-                  Grafana 10. It’s recommended to use
-                  <literal>services.grafana.provision.alerting.contactPoints</literal>
-                  instead.
-                </para>
-              </listitem>
-            </itemizedlist>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.grafana.provision.alerting</literal>
-          option was added. It includes suboptions for every
-          alerting-related objects (with the exception of
-          <literal>notifiers</literal>), which means it’s now possible
-          to configure modern Grafana alerting declaratively.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          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>
-          The <literal>diamond</literal> package has been update from
-          0.8.36 to 2.0.15. See the
-          <link xlink:href="https://github.com/bbuchfink/diamond/releases">upstream
-          release notes</link> for more details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>guake</literal> package has been updated from
-          3.6.3 to 3.9.0, see the
-          <link xlink:href="https://github.com/Guake/guake/releases">changelog</link>
-          for more details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>netlify-cli</literal> package has been updated
-          from 6.13.2 to 12.2.4, see the
-          <link xlink:href="https://github.com/netlify/cli/releases">changelog</link>
-          for more details.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>dockerTools.buildImage</literal>’s
-          <literal>contents</literal> parameter has been deprecated 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>
-          The <literal>proxmox.qemuConf.bios</literal> option was added,
-          it corresponds to <literal>Hardware-&gt;BIOS</literal> field
-          in Proxmox web interface. Use
-          <literal>&quot;ovmf&quot;</literal> value to build UEFI image,
-          default value remains <literal>&quot;bios&quot;</literal>. New
-          option <literal>proxmox.partitionTableType</literal> defaults
-          to either <literal>&quot;legacy&quot;</literal> or
-          <literal>&quot;efi&quot;</literal>, depending on the
-          <literal>bios</literal> value. Setting
-          <literal>partitionTableType</literal> to
-          <literal>&quot;hybrid&quot;</literal> results in an image,
-          which supports both methods
-          (<literal>&quot;bios&quot;</literal> and
-          <literal>&quot;ovmf&quot;</literal>), thereby remaining
-          bootable after change to Proxmox
-          <literal>Hardware-&gt;BIOS</literal> field.
-        </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>
-          Option descriptions, examples, and defaults writing in DocBook
-          are now deprecated. Using CommonMark is preferred and will
-          become the default in a future release.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <literal>documentation.nixos.options.allowDocBook</literal>
-          option was added to ease the transition to CommonMark option
-          documentation. Setting this option to <literal>false</literal>
-          causes an error for every option included in the manual that
-          uses DocBook documentation; it defaults to
-          <literal>true</literal> to preserve the previous behavior and
-          will be removed once the transition to CommonMark is complete.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Redis module now persists each instance’s configuration
-          file in the state directory, in order to support some more
-          advanced use cases like Sentinel.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>protonup</literal> has been aliased to and replaced
-          by <literal>protonup-ng</literal> due to upstream not
-          maintaining it.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The udisks2 service, available at
-          <literal>services.udisks2.enable</literal>, is now disabled by
-          default. It will automatically be enabled through services and
-          desktop environments as needed. This also means that polkit
-          will now actually be disabled by default. The default for
-          <literal>security.polkit.enable</literal> was already flipped
-          in the previous release, but udisks2 being enabled by default
-          re-enabled it.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Nextcloud has been updated to version
-          <emphasis role="strong">25</emphasis>. Additionally the
-          following things have changed for Nextcloud in NixOS:
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              For Nextcloud <emphasis role="strong">&gt;=24</emphasis>,
-              the default PHP version is 8.1.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Nextcloud <emphasis role="strong">23</emphasis> has been
-              removed since it will reach its
-              <link xlink:href="https://github.com/nextcloud/server/wiki/Maintenance-and-Release-Schedule/d76576a12a626d53305d480a6065b57cab705d3d">end
-              of life in December 2022</link>.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              If <literal>system.stateVersion</literal> is
-              <emphasis role="strong">&gt;=22.11</emphasis>, Nextcloud
-              25 will be installed by default. For older versions,
-              Nextcloud 24 will be installed.
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              Please ensure that you only upgrade one major release at a
-              time! Nextcloud doesn’t support upgrades across multiple
-              versions, i.e. an upgrade from
-              <emphasis role="strong">23</emphasis> to
-              <emphasis role="strong">25</emphasis> is only possible
-              when upgrading to <emphasis role="strong">24</emphasis>
-              first.
-            </para>
-          </listitem>
-        </itemizedlist>
-      </listitem>
-      <listitem>
-        <para>
-          systemd-oomd is enabled by default. Depending on which systemd
-          units have <literal>ManagedOOMSwap=kill</literal> or
-          <literal>ManagedOOMMemoryPressure=kill</literal>, systemd-oomd
-          will SIGKILL all the processes under the appropriate
-          descendant cgroups when the configured limits are exceeded.
-          NixOS does currently not configure cgroups with oomd by
-          default, this can be enabled using
-          <link xlink:href="options.html#opt-systemd.oomd.enableRootSlice">systemd.oomd.enableRootSlice</link>,
-          <link xlink:href="options.html#opt-systemd.oomd.enableSystemSlice">systemd.oomd.enableSystemSlice</link>,
-          and
-          <link xlink:href="options.html#opt-systemd.oomd.enableUserServices">systemd.oomd.enableUserServices</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>tt-rss</literal> service performs two database
-          migrations when you first use its web UI after upgrade.
-          Consider backing up its database before updating.
-        </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>
-          The Wordpress module now has support for installing language
-          packs through a new option,
-          <literal>services.wordpress.sites.&lt;site&gt;.languages</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The default package for
-          <literal>services.mullvad-vpn.package</literal> was changed to
-          <literal>pkgs.mullvad</literal>, allowing cross-platform usage
-          of Mullvad. <literal>pkgs.mullvad</literal> only contains the
-          Mullvad CLI tool, so users who rely on the Mullvad GUI will
-          want to change it back to <literal>pkgs.mullvad-vpn</literal>,
-          or add <literal>pkgs.mullvad-vpn</literal> to their
-          environment.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          PowerDNS has been updated from v4.6.2 to v4.7.2. Please be
-          sure to review the
-          <link xlink:href="https://doc.powerdns.com/authoritative/upgrading.html#to-4-7-0-or-master">Upgrade
-          Notes</link> provided by upstream before upgrading. Worth
-          specifically noting is that the new Catalog Zones feature
-          comes with a mandatory schema change for the GSQL database
-          backends, which has to be manually applied.
-        </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 may be removed
-          in a future release.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          There is a new module for <literal>xfconf</literal> (the Xfce
-          configuration storage system), which has a dbus service.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Mastodon package has been upgraded to v4.0.0. See the
-          <link xlink:href="https://github.com/mastodon/mastodon/releases/tag/v4.0.0">v4.0.0
-          release notes</link> for a list of changes. On standard
-          setups, no manual migration steps are required. Nevertheless,
-          a database backup is recommended.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>nomad</literal> package now defaults to v1.3,
-          which no longer has a downgrade path to v1.2 or older.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>nodePackages</literal> package set now defaults
-          to the LTS release in the <literal>nodejs</literal> package
-          again, instead of being pinned to
-          <literal>nodejs-14_x</literal>. Several updates to node2nix
-          have been made for compatibility with newer Node.js and npm
-          versions and a new <literal>postRebuild</literal> hook has
-          been added for packages to perform extra build steps before
-          the npm install step prunes dev dependencies.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>boot.kernel.sysctl</literal> is defined as a
-          freeformType and adds a custom merge option for
-          <literal>net.core.rmem_max</literal> (taking the highest value
-          defined to avoid conflicts between 2 services trying to set
-          that value).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>mame</literal> package does not ship with its
-          tools anymore in the default output. They were moved to a
-          separate <literal>tools</literal> output instead. For
-          convenience, <literal>mame-tools</literal> package was added
-          for those who want to use it.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          A NixOS module for Firefox has been added which allows
-          preferences and
-          <link xlink:href="https://github.com/mozilla/policy-templates/blob/master/README.md">policies</link>
-          to be set. This also allows extensions to be installed via the
-          <literal>ExtensionSettings</literal> policy. The new options
-          are under <literal>programs.firefox</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The option
-          <literal>services.picom.experimentalBackends</literal> was
-          removed since it is now the default and the option will cause
-          <literal>picom</literal> to quit instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>haskellPackages.callHackage</literal> is not always
-          invalidated if <literal>all-cabal-hashes</literal> changes,
-          leading to less rebuilds of haskell dependencies.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>haskellPackages.callHackage</literal> and
-          <literal>haskellPackages.callCabal2nix</literal> (and related
-          functions) no longer keep a reference to the
-          <literal>cabal2nix</literal> call used to generate them. As a
-          result, they will be garbage collected more often.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-22.11-new-services">
-    <title>New Services</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <link xlink:href="https://git.sr.ht/~migadu/alps">alps</link>,
-          a simple and extensible webmail. Available as
-          <link linkend="opt-services.alps.enable">services.alps</link>.
-        </para>
-      </listitem>
-      <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://www.ausweisapp.bund.de/">AusweisApp2</link>,
-          the authentication software for the German ID card. Available
-          as
-          <link linkend="opt-programs.ausweisapp.enable">programs.ausweisapp</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/maxbrunet/automatic-timezoned">automatic-timezoned</link>.
-          a Linux daemon to automatically update the system timezone
-          based on location. Available as
-          <link linkend="opt-services.automatic-timezoned.enable">services.automatic-timezoned</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.dolibarr.org/">Dolibarr</link>,
-          an enterprise resource planning and customer relationship
-          manager. Enable using
-          <link linkend="opt-services.dolibarr.enable">services.dolibarr</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/shizunge/endlessh-go">endlessh-go</link>,
-          an SSH tarpit that exposes Prometheus metrics. Available as
-          <link linkend="opt-services.endlessh-go.enable">services.endlessh-go</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/skeeto/endlessh">endlessh</link>,
-          an SSH tarpit. Available as
-          <link linkend="opt-services.endlessh.enable">services.endlessh</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://evcc.io">EVCC</link> is an EV charge
-          controller with PV integration. It supports a multitude of
-          chargers, meters, vehicle APIs and more and ties that together
-          with a well-tested backend and a lightweight web frontend.
-          Available as
-          <link linkend="opt-services.evcc.enable">services.evcc</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://freshrss.org/">FreshRSS</link>, a
-          free, self-hostable RSS feed aggregator. Available as
-          <link linkend="opt-services.freshrss.enable">services.freshrss</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://garagehq.deuxfleurs.fr/">Garage</link>,
-          a simple object storage server for geodistributed deployments,
-          alternative to MinIO. Available as
-          <link linkend="opt-services.garage.enable">services.garage</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/L11R/go-autoconfig">go-autoconfig</link>,
-          IMAP/SMTP autodiscover server. Available as
-          <link linkend="opt-services.go-autoconfig.enable">services.go-autoconfig</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>
-      <listitem>
-        <para>
-          <link xlink:href="https://hbase.apache.org/">HBase
-          cluster</link>, a distributed, scalable, big data store.
-          Available as
-          <link xlink:href="options.html#opt-services.hadoop.hbase.enable">services.hadoop.hbase</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/jtroo/kanata">kanata</link>,
-          a tool to improve keyboard comfort and usability with advanced
-          customization. Available as
-          <link xlink:href="options.html#opt-services.kanata.enable">services.kanata</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/prymitive/karma">karma</link>,
-          an alert dashboard for Prometheus Alertmanager. Available as
-          <link xlink:href="options.html#opt-services.karma.enable">services.karma</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://komga.org/">Komga</link>, a free and
-          open source comics/mangas media server. Available as
-          <link linkend="opt-services.komga.enable">services.komga</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/prymitive/kthxbye">kthxbye</link>,
-          an alert acknowledgement management daemon for Prometheus
-          Alertmanager. Available as
-          <link xlink:href="options.html#opt-services.kthxbye.enable">services.kthxbye</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://languagetool.org/">languagetool</link>,
-          a multilingual grammar, style, and spell checker. Available as
-          <link xlink:href="options.html#opt-services.languagetool.enable">services.languagetool</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://listmonk.app">Listmonk</link>, a
-          self-hosted newsletter manager. Enable using
-          <link xlink:href="options.html#opt-services.listmonk.enable">services.listmonk</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://mepo.milesalan.com">Mepo</link>, a
-          fast, simple, hackable OSM map viewer for mobile and desktop
-          Linux. Available as
-          <link linkend="opt-programs.mepo.enable">programs.mepo.enable</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://troglobit.com/projects/merecat/">merecat</link>,
-          a small and easy HTTP server based on thttpd. Available as
-          <link linkend="opt-services.merecat.enable">services.merecat</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://netbird.io">netbird</link>, a zero
-          configuration VPN. Available as
-          <link xlink:href="options.html#opt-services.netbird.enable">services.netbird</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://ntfy.sh">ntfy.sh</link>, a push
-          notification service. Available as
-          <link linkend="opt-services.ntfy-sh.enable">services.ntfy-sh</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://gitlab.com/CalcProgrammer1/OpenRGB/-/tree/master">OpenRGB</link>,
-          a FOSS tool for controlling RGB lighting. Available as
-          <link xlink:href="options.html#opt-services.hardware.openrgb.enable">services.hardware.openrgb.enable</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://www.getoutline.com/">Outline</link>,
-          a wiki and knowledge base similar to Notion. Available as
-          <link linkend="opt-services.outline.enable">services.outline</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/zalando/patroni">Patroni</link>,
-          a template for PostgreSQL HA with ZooKeeper, etcd or Consul.
-          Available as
-          <link xlink:href="options.html#opt-services.patroni.enable">services.patroni</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://github.com/edneville/please">Please</link>,
-          a Sudo clone written in Rust. Available as
-          <link linkend="opt-security.please.enable">security.please</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/prometheus-community/ipmi_exporter">Prometheus
-          IPMI exporter</link>, an IPMI exporter for Prometheus.
-          Available as
-          <link linkend="opt-services.prometheus.exporters.ipmi.enable">services.prometheus.exporters.ipmi</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/messagebird/sachet/">Sachet</link>,
-          an SMS alerting tool for the Prometheus Alertmanager.
-          Available as
-          <link linkend="opt-services.prometheus.sachet.enable">services.prometheus.sachet</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://github.com/mozilla-services/syncstorage-rs">syncstorage-rs</link>,
-          a self-hostable sync server for Firefox. Available as
-          <link xlink:href="options.html#opt-services.firefox-syncserver.enable">services.firefox-syncserver</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://tandoor.dev">Tandoor Recipes</link>,
-          a self-hosted multi-tenant recipe collection. Available as
-          <link xlink:href="options.html#opt-services.tandoor-recipes.enable">services.tandoor-recipes</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="http://www.litech.org/tayga/">TAYGA</link>,
-          an out-of-kernel stateless NAT64 implementation. Available as
-          <link linkend="opt-services.tayga.enable">services.tayga</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/tmate-io/tmate-ssh-server">tmate-ssh-server</link>,
-          server side part of
-          <link xlink:href="https://tmate.io/">tmate</link>. Available
-          as
-          <link linkend="opt-services.tmate-ssh-server.enable">services.tmate-ssh-server</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://uptime.kuma.pet/">Uptime
-          Kuma</link>, a fancy self-hosted monitoring tool. Available as
-          <link linkend="opt-services.uptime-kuma.enable">services.uptime-kuma</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://writefreely.org">WriteFreely</link>,
-          a simple blogging platform with ActivityPub support. Available
-          as
-          <link xlink:href="options.html#opt-services.writefreely.enable">services.writefreely</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/XTLS/Xray-core">xray</link>,
-          a fully compatible v2ray-core replacement. Features XTLS,
-          which when enabled on server and client, brings UDP FullCone
-          NAT to proxy setups. Available as
-          <link xlink:href="options.html#opt-services.xray.enable">services.xray</link>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</section>
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
deleted file mode 100644
index 657b5c6f26d..00000000000
--- a/nixos/doc/manual/from_md/release-notes/rl-2305.section.xml
+++ /dev/null
@@ -1,337 +0,0 @@
-<section xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-release-23.05">
-  <title>Release 23.05 (“Stoat”, 2023.05/??)</title>
-  <para>
-    Support is planned until the end of December 2023, handing over to
-    23.11.
-  </para>
-  <section xml:id="sec-release-23.05-highlights">
-    <title>Highlights</title>
-    <para>
-      In addition to numerous new and upgraded packages, this release
-      has the following highlights:
-    </para>
-    <itemizedlist spacing="compact">
-      <listitem>
-        <para>
-          Cinnamon has been updated to 5.6, see
-          <link xlink:href="https://github.com/NixOS/nixpkgs/pull/201328#issue-1449910204">the
-          pull request</link> for what is changed.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-23.05-new-services">
-    <title>New Services</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/akinomyoga/ble.sh">blesh</link>,
-          a line editor written in pure bash. Available as
-          <link linkend="opt-programs.bash.blesh.enable">programs.bash.blesh</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/junegunn/fzf">fzf</link>,
-          a command line fuzzyfinder. Available as
-          <link linkend="opt-programs.fzf.fuzzyCompletion">programs.fzf</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://github.com/ellie/atuin">atuin</link>,
-          a sync server for shell history. Available as
-          <link linkend="opt-services.atuin.enable">services.atuin</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://gitlab.com/kop316/mmsd">mmsd</link>,
-          a lower level daemon that transmits and recieves MMSes.
-          Available as
-          <link linkend="opt-services.mmsd.enable">services.mmsd</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <link xlink:href="https://v2raya.org">v2rayA</link>, a Linux
-          web GUI client of Project V which supports V2Ray, Xray, SS,
-          SSR, Trojan and Pingtunnel. Available as
-          <link xlink:href="options.html#opt-services.v2raya.enable">services.v2raya</link>.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-23.05-incompatibilities">
-    <title>Backward Incompatibilities</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>carnix</literal> and <literal>cratesIO</literal> has
-          been removed due to being unmaintained, use alternatives such
-          as
-          <link xlink:href="https://github.com/nix-community/naersk">naersk</link>
-          and
-          <link xlink:href="https://github.com/kolloch/crate2nix">crate2nix</link>
-          instead.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>borgbackup</literal> module now has an option for
-          inhibiting system sleep while backups are running, defaulting
-          to off (not inhibiting sleep), available as
-          <link linkend="opt-services.borgbackup.jobs._name_.inhibitsSleep"><literal>services.borgbackup.jobs.&lt;name&gt;.inhibitsSleep</literal></link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The EC2 image module no longer fetches instance metadata in
-          stage-1. This results in a significantly smaller initramfs,
-          since network drivers no longer need to be included, and
-          faster boots, since metadata fetching can happen in parallel
-          with startup of other services. This breaks services which
-          rely on metadata being present by the time stage-2 is entered.
-          Anything which reads EC2 metadata from
-          <literal>/etc/ec2-metadata</literal> should now have an
-          <literal>after</literal> dependency on
-          <literal>fetch-ec2-metadata.service</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.sourcehut.dispatch</literal> and the
-          corresponding package
-          (<literal>sourcehut.dispatchsrht</literal>) have been removed
-          due to
-          <link xlink:href="https://sourcehut.org/blog/2022-08-01-dispatch-deprecation-plans/">upstream
-          deprecation</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link linkend="opt-services.snapserver.openFirewall">services.snapserver.openFirewall</link>
-          module option default value has been changed from
-          <literal>true</literal> to <literal>false</literal>. You will
-          need to explicitly set this option to <literal>true</literal>,
-          or configure your firewall.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link linkend="opt-services.tmate-ssh-server.openFirewall">services.tmate-ssh-server.openFirewall</link>
-          module option default value has been changed from
-          <literal>true</literal> to <literal>false</literal>. You will
-          need to explicitly set this option to <literal>true</literal>,
-          or configure your firewall.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The
-          <link linkend="opt-services.unifi-video.openFirewall">services.unifi-video.openFirewall</link>
-          module option default value has been changed from
-          <literal>true</literal> to <literal>false</literal>. You will
-          need to explicitly set this option to <literal>true</literal>,
-          or configure your firewall.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The Nginx module now validates the syntax of config files at
-          build time. For more complex configurations (using
-          <literal>include</literal> with out-of-store files notably)
-          you may need to disable this check by setting
-          <link linkend="opt-services.nginx.validateConfig">services.nginx.validateConfig</link>
-          to <literal>false</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The EC2 image module previously detected and automatically
-          mounted ext3-formatted instance store devices and partitions
-          in stage-1 (initramfs), storing <literal>/tmp</literal> on the
-          first discovered device. This behaviour, which only catered to
-          very specific use cases and could not be disabled, has been
-          removed. Users relying on this should provide their own
-          implementation, and probably use ext4 and perform the mount in
-          stage-2.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The EC2 image module previously detected and activated
-          swap-formatted instance store devices and partitions in
-          stage-1 (initramfs). This behaviour has been removed. Users
-          relying on this should provide their own implementation.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Qt 5.12 and 5.14 have been removed, as the corresponding
-          branches have been EOL upstream for a long time. This affected
-          under 10 packages in nixpkgs, largely unmaintained upstream as
-          well, however, out-of-tree package expressions may need to be
-          updated manually.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          In <literal>mastodon</literal> it is now necessary to specify
-          location of file with <literal>PostgreSQL</literal> database
-          password. In
-          <literal>services.mastodon.database.passwordFile</literal>
-          parameter default value
-          <literal>/var/lib/mastodon/secrets/db-password</literal> has
-          been changed to <literal>null</literal>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>nix.readOnlyStore</literal> option has been
-          renamed to <literal>boot.readOnlyNixStore</literal> to clarify
-          that it configures the NixOS boot process, not the Nix daemon.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="sec-release-23.05-notable-changes">
-    <title>Other Notable Changes</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>vim_configurable</literal> has been renamed to
-          <literal>vim-full</literal> to avoid confusion:
-          <literal>vim-full</literal>’s build-time features are
-          configurable, but both <literal>vim</literal> and
-          <literal>vim-full</literal> are
-          <emphasis>customizable</emphasis> (in the sense of user
-          configuration, like vimrc).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The module for the application firewall
-          <literal>opensnitch</literal> got the ability to configure
-          rules. Available as
-          <link linkend="opt-services.opensnitch.rules">services.opensnitch.rules</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The module <literal>usbmuxd</literal> now has the ability to
-          change the package used by the daemon. In case you’re
-          experiencing issues with <literal>usbmuxd</literal> you can
-          try an alternative program like <literal>usbmuxd2</literal>.
-          Available as
-          <link linkend="opt-services.usbmuxd.package">services.usbmuxd.package</link>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>services.mastodon</literal> gained a tootctl wrapped
-          named <literal>mastodon-tootctl</literal> similar to
-          <literal>nextcloud-occ</literal> which can be executed from
-          any user and switches to the configured mastodon user with
-          sudo and sources the environment variables.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>dnsmasq</literal> service now takes configuration
-          via the <literal>services.dnsmasq.settings</literal> attribute
-          set. The option
-          <literal>services.dnsmasq.extraConfig</literal> will be
-          deprecated when NixOS 22.11 reaches end of life.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          To reduce closure size in
-          <literal>nixos/modules/profiles/minimal.nix</literal> profile
-          disabled installation documentations and manuals. Also
-          disabled <literal>logrotate</literal> and
-          <literal>udisks2</literal> services.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The minimal ISO image now uses the
-          <literal>nixos/modules/profiles/minimal.nix</literal> profile.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>mastodon</literal> now supports connection to a
-          remote <literal>PostgreSQL</literal> database.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          A new <literal>virtualisation.rosetta</literal> module was
-          added to allow running <literal>x86_64</literal> binaries
-          through
-          <link xlink:href="https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment">Rosetta</link>
-          inside virtualised NixOS guests on Apple silicon. This feature
-          works by default with the
-          <link xlink:href="https://docs.getutm.app/">UTM</link>
-          virtualisation
-          <link xlink:href="https://search.nixos.org/packages?channel=unstable&amp;show=utm&amp;from=0&amp;size=1&amp;sort=relevance&amp;type=packages&amp;query=utm">package</link>.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The new option <literal>users.motdFile</literal> allows
-          configuring a Message Of The Day that can be updated
-          dynamically.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Enabling global redirect in
-          <literal>services.nginx.virtualHosts</literal> now allows one
-          to add exceptions with the <literal>locations</literal>
-          option.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Resilio sync secret keys can now be provided using a secrets
-          file at runtime, preventing these secrets from ending up in
-          the Nix store.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>services.fwupd</literal> module now allows
-          arbitrary daemon settings to be configured in a structured
-          manner
-          (<link linkend="opt-services.fwupd.daemonSettings"><literal>services.fwupd.daemonSettings</literal></link>).
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The <literal>unifi-poller</literal> package and corresponding
-          NixOS module have been renamed to <literal>unpoller</literal>
-          to match upstream.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          The new option
-          <literal>services.tailscale.useRoutingFeatures</literal>
-          controls various settings for using Tailscale features like
-          exit nodes and subnet routers. If you wish to use your machine
-          as an exit node, you can set this setting to
-          <literal>server</literal>, otherwise if you wish to use an
-          exit node you can set this setting to
-          <literal>client</literal>. The strict RPF warning has been
-          removed as the RPF will be loosened automatically based on the
-          value of this setting.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</section>
diff --git a/nixos/doc/manual/installation/changing-config.chapter.md b/nixos/doc/manual/installation/changing-config.chapter.md
index 8a404f085d7..11b49ccb1f6 100644
--- a/nixos/doc/manual/installation/changing-config.chapter.md
+++ b/nixos/doc/manual/installation/changing-config.chapter.md
@@ -13,7 +13,7 @@ booting, and try to realise the configuration in the running system
 (e.g., by restarting system services).
 
 ::: {.warning}
-This command doesn\'t start/stop [user services](#opt-systemd.user.services)
+This command doesn't start/stop [user services](#opt-systemd.user.services)
 automatically. `nixos-rebuild` only runs a `daemon-reload` for each user with running
 user services.
 :::
@@ -51,7 +51,7 @@ GRUB 2 boot screen by giving it a different *profile name*, e.g.
 ```
 
 which causes the new configuration (and previous ones created using
-`-p test`) to show up in the GRUB submenu "NixOS - Profile \'test\'".
+`-p test`) to show up in the GRUB submenu "NixOS - Profile 'test'".
 This can be useful to separate test configurations from "stable"
 configurations.
 
diff --git a/nixos/doc/manual/installation/installation.md b/nixos/doc/manual/installation/installation.md
new file mode 100644
index 00000000000..14059425660
--- /dev/null
+++ b/nixos/doc/manual/installation/installation.md
@@ -0,0 +1,11 @@
+# Installation {#ch-installation}
+
+This section describes how to obtain, install, and configure NixOS for first-time use.
+
+```{=include=} chapters
+obtaining.chapter.md
+installing.chapter.md
+changing-config.chapter.md
+upgrading.chapter.md
+building-nixos.chapter.md
+```
diff --git a/nixos/doc/manual/installation/installation.xml b/nixos/doc/manual/installation/installation.xml
deleted file mode 100644
index ba07d71d0ca..00000000000
--- a/nixos/doc/manual/installation/installation.xml
+++ /dev/null
@@ -1,18 +0,0 @@
-<part 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="ch-installation">
- <title>Installation</title>
- <partintro xml:id="ch-installation-intro">
-  <para>
-   This section describes how to obtain, install, and configure NixOS for
-   first-time use.
-  </para>
- </partintro>
- <xi:include href="../from_md/installation/obtaining.chapter.xml" />
- <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 36ef29d4463..921592fe535 100644
--- a/nixos/doc/manual/installation/installing-from-other-distro.section.md
+++ b/nixos/doc/manual/installation/installing-from-other-distro.section.md
@@ -30,7 +30,7 @@ The first steps to all these are the same:
 
 1.  Switch to the NixOS channel:
 
-    If you\'ve just installed Nix on a non-NixOS distribution, you will
+    If you've just installed Nix on a non-NixOS distribution, you will
     be on the `nixpkgs` channel by default.
 
     ```ShellSession
@@ -49,10 +49,10 @@ The first steps to all these are the same:
 
 1.  Install the NixOS installation tools:
 
-    You\'ll need `nixos-generate-config` and `nixos-install`, but this
+    You'll need `nixos-generate-config` and `nixos-install`, but this
     also makes some man pages and `nixos-enter` available, just in case
     you want to chroot into your NixOS partition. NixOS installs these
-    by default, but you don\'t have NixOS yet..
+    by default, but you don't have NixOS yet..
 
     ```ShellSession
     $ nix-env -f '<nixpkgs>' -iA nixos-install-tools
@@ -70,7 +70,7 @@ The first steps to all these are the same:
     refer to the partitioning, file-system creation, and mounting steps
     of [](#sec-installation)
 
-    If you\'re about to install NixOS in place using `NIXOS_LUSTRATE`
+    If you're about to install NixOS in place using `NIXOS_LUSTRATE`
     there is nothing to do for this step.
 
 1.  Generate your NixOS configuration:
@@ -79,12 +79,12 @@ The first steps to all these are the same:
     $ sudo `which nixos-generate-config` --root /mnt
     ```
 
-    You\'ll probably want to edit the configuration files. Refer to the
+    You'll probably want to edit the configuration files. Refer to the
     `nixos-generate-config` step in [](#sec-installation) for more
     information.
 
     Consider setting up the NixOS bootloader to give you the ability to
-    boot on your existing Linux partition. For instance, if you\'re
+    boot on your existing Linux partition. For instance, if you're
     using GRUB and your existing distribution is running Ubuntu, you may
     want to add something like this to your `configuration.nix`:
 
@@ -152,15 +152,15 @@ The first steps to all these are the same:
     ```
 
     Note that this will place the generated configuration files in
-    `/etc/nixos`. You\'ll probably want to edit the configuration files.
+    `/etc/nixos`. You'll probably want to edit the configuration files.
     Refer to the `nixos-generate-config` step in
     [](#sec-installation) for more information.
 
-    You\'ll likely want to set a root password for your first boot using
-    the configuration files because you won\'t have a chance to enter a
+    You'll likely want to set a root password for your first boot using
+    the configuration files because you won't have a chance to enter a
     password until after you reboot. You can initialize the root password
-    to an empty one with this line: (and of course don\'t forget to set
-    one once you\'ve rebooted or to lock the account with
+    to an empty one with this line: (and of course don't forget to set
+    one once you've rebooted or to lock the account with
     `sudo passwd -l root` if you use `sudo`)
 
     ```nix
@@ -186,7 +186,7 @@ The first steps to all these are the same:
     bootup scripts require its presence).
 
     `/etc/NIXOS_LUSTRATE` tells the NixOS bootup scripts to move
-    *everything* that\'s in the root partition to `/old-root`. This will
+    *everything* that's in the root partition to `/old-root`. This will
     move your existing distribution out of the way in the very early
     stages of the NixOS bootup. There are exceptions (we do need to keep
     NixOS there after all), so the NixOS lustrate process will not
@@ -201,10 +201,10 @@ The first steps to all these are the same:
 
     ::: {.note}
     Support for `NIXOS_LUSTRATE` was added in NixOS 16.09. The act of
-    \"lustrating\" refers to the wiping of the existing distribution.
+    "lustrating" refers to the wiping of the existing distribution.
     Creating `/etc/NIXOS_LUSTRATE` can also be used on NixOS to remove
-    all mutable files from your root partition (anything that\'s not in
-    `/nix` or `/boot` gets \"lustrated\" on the next boot.
+    all mutable files from your root partition (anything that's not in
+    `/nix` or `/boot` gets "lustrated" on the next boot.
 
     lustrate /ˈlʌstreɪt/ verb.
 
@@ -212,14 +212,14 @@ The first steps to all these are the same:
     ritual action.
     :::
 
-    Let\'s create the files:
+    Let's create the files:
 
     ```ShellSession
     $ sudo touch /etc/NIXOS
     $ sudo touch /etc/NIXOS_LUSTRATE
     ```
 
-    Let\'s also make sure the NixOS configuration files are kept once we
+    Let's also make sure the NixOS configuration files are kept once we
     reboot on NixOS:
 
     ```ShellSession
@@ -233,7 +233,7 @@ The first steps to all these are the same:
 
     ::: {.warning}
     Once you complete this step, your current distribution will no
-    longer be bootable! If you didn\'t get all the NixOS configuration
+    longer be bootable! If you didn't get all the NixOS configuration
     right, especially those settings pertaining to boot loading and root
     partition, NixOS may not be bootable either. Have a USB rescue
     device ready in case this happens.
@@ -247,7 +247,7 @@ The first steps to all these are the same:
     Cross your fingers, reboot, hopefully you should get a NixOS prompt!
 
 1.  If for some reason you want to revert to the old distribution,
-    you\'ll need to boot on a USB rescue disk and do something along
+    you'll need to boot on a USB rescue disk and do something along
     these lines:
 
     ```ShellSession
@@ -264,14 +264,14 @@ The first steps to all these are the same:
     This may work as is or you might also need to reinstall the boot
     loader.
 
-    And of course, if you\'re happy with NixOS and no longer need the
+    And of course, if you're happy with NixOS and no longer need the
     old distribution:
 
     ```ShellSession
     sudo rm -rf /old-root
     ```
 
-1.  It\'s also worth noting that this whole process can be automated.
+1.  It's also worth noting that this whole process can be automated.
     This is especially useful for Cloud VMs, where provider do not
     provide NixOS. For instance,
     [nixos-infect](https://github.com/elitak/nixos-infect) uses the
diff --git a/nixos/doc/manual/installation/installing-kexec.section.md b/nixos/doc/manual/installation/installing-kexec.section.md
index 286cbbda6a6..61d8e8e5999 100644
--- a/nixos/doc/manual/installation/installing-kexec.section.md
+++ b/nixos/doc/manual/installation/installing-kexec.section.md
@@ -30,7 +30,7 @@ This will create a `result` directory containing the following:
 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
+Note its 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
diff --git a/nixos/doc/manual/installation/installing-usb.section.md b/nixos/doc/manual/installation/installing-usb.section.md
index da32935a7a1..adfe22ea2f0 100644
--- a/nixos/doc/manual/installation/installing-usb.section.md
+++ b/nixos/doc/manual/installation/installing-usb.section.md
@@ -56,12 +56,12 @@ select the image, select the USB flash drive and click "Write".
   sudo dd if=<path-to-image> of=/dev/rdiskX bs=4m
   ```
 
-  After `dd` completes, a GUI dialog \"The disk
-  you inserted was not readable by this computer\" will pop up, which can
+  After `dd` completes, a GUI dialog "The disk
+  you inserted was not readable by this computer" will pop up, which can
   be ignored.
 
   ::: {.note}
-  Using the \'raw\' `rdiskX` device instead of `diskX` with dd completes in
+  Using the 'raw' `rdiskX` device instead of `diskX` with dd completes in
   minutes instead of hours.
   :::
 
diff --git a/nixos/doc/manual/installation/installing-virtualbox-guest.section.md b/nixos/doc/manual/installation/installing-virtualbox-guest.section.md
index c3bbfe12152..004838e586b 100644
--- a/nixos/doc/manual/installation/installing-virtualbox-guest.section.md
+++ b/nixos/doc/manual/installation/installing-virtualbox-guest.section.md
@@ -6,7 +6,7 @@ use a pre-made VirtualBox appliance, it is available at [the downloads
 page](https://nixos.org/nixos/download.html). If you want to set up a
 VirtualBox guest manually, follow these instructions:
 
-1.  Add a New Machine in VirtualBox with OS Type \"Linux / Other Linux\"
+1.  Add a New Machine in VirtualBox with OS Type "Linux / Other Linux"
 
 1.  Base Memory Size: 768 MB or higher.
 
@@ -16,7 +16,7 @@ VirtualBox guest manually, follow these instructions:
 
 1.  Click on Settings / System / Processor and enable PAE/NX
 
-1.  Click on Settings / System / Acceleration and enable \"VT-x/AMD-V\"
+1.  Click on Settings / System / Acceleration and enable "VT-x/AMD-V"
     acceleration
 
 1.  Click on Settings / Display / Screen and select VMSVGA as Graphics
@@ -41,7 +41,7 @@ boot.initrd.checkJournalingFS = false;
 
 Shared folders can be given a name and a path in the host system in the
 VirtualBox settings (Machine / Settings / Shared Folders, then click on
-the \"Add\" icon). Add the following to the
+the "Add" icon). Add the following to the
 `/etc/nixos/configuration.nix` to auto-mount them. If you do not add
 `"nofail"`, the system will not boot properly.
 
diff --git a/nixos/doc/manual/installation/installing.chapter.md b/nixos/doc/manual/installation/installing.chapter.md
index 04bc7b1f207..53cf9ed14c3 100644
--- a/nixos/doc/manual/installation/installing.chapter.md
+++ b/nixos/doc/manual/installation/installing.chapter.md
@@ -15,16 +15,16 @@ To begin the installation, you have to boot your computer from the install drive
 
      ::: {.note}
      The key to open the boot menu is different across computer brands and even
-     models. It can be <kbd>F12</kbd>, but also <kbd>F1</kbd>,
-     <kbd>F9</kbd>, <kbd>F10</kbd>, <kbd>Enter</kbd>, <kbd>Del</kbd>,
-     <kbd>Esc</kbd> or another function key. If you are unsure and don't see
+     models. It can be [F12]{.keycap}, but also [F1]{.keycap},
+     [F9]{.keycap}, [F10]{.keycap}, [Enter]{.keycap}, [Del]{.keycap},
+     [Esc]{.keycap} or another function key. If you are unsure and don't see
      it on the early boot screen, you can search online for your computers
      brand, model followed by "boot from usb".
      The computer might not even have that feature, so you have to go into the
      BIOS/UEFI settings to change the boot order. Again, search online for
      details about your specific computer model.
 
-     For Apple computers with Intel processors press and hold the <kbd>⌥</kbd>
+     For Apple computers with Intel processors press and hold the [⌥]{.keycap}
      (Option or Alt) key until you see the boot menu. On Apple silicon press
      and hold the power button.
      :::
@@ -41,7 +41,7 @@ To begin the installation, you have to boot your computer from the install drive
 
 3.   Shortly after selecting the appropriate boot drive, you should be
      presented with a menu with different installer options. Leave the default
-     and wait (or press <kbd>Enter</kbd> to speed up).
+     and wait (or press [Enter]{.keycap} to speed up).
 
 4.   The graphical images will start their corresponding desktop environment
      and the graphical installer, which can take some time. The minimal images
@@ -230,11 +230,11 @@ The recommended partition scheme differs depending if the computer uses
 #### UEFI (GPT) {#sec-installation-manual-partitioning-UEFI}
 []{#sec-installation-partitioning-UEFI} <!-- legacy anchor -->
 
-Here\'s an example partition scheme for UEFI, using `/dev/sda` as the
+Here's an example partition scheme for UEFI, using `/dev/sda` as the
 device.
 
 ::: {.note}
-You can safely ignore `parted`\'s informational message about needing to
+You can safely ignore `parted`'s informational message about needing to
 update /etc/fstab.
 :::
 
@@ -279,11 +279,11 @@ Once complete, you can follow with
 #### Legacy Boot (MBR) {#sec-installation-manual-partitioning-MBR}
 []{#sec-installation-partitioning-MBR} <!-- legacy anchor -->
 
-Here\'s an example partition scheme for Legacy Boot, using `/dev/sda` as
+Here's an example partition scheme for Legacy Boot, using `/dev/sda` as
 the device.
 
 ::: {.note}
-You can safely ignore `parted`\'s informational message about needing to
+You can safely ignore `parted`'s informational message about needing to
 update /etc/fstab.
 :::
 
@@ -421,14 +421,14 @@ 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
+        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 select a boot-loader, either system-boot or GRUB. The recommended
+    :   You must select a boot-loader, either systemd-boot or GRUB. The recommended
         option is systemd-boot: set the option [](#opt-boot.loader.systemd-boot.enable)
         to `true`. `nixos-generate-config` should do this automatically
         for new configurations when booted in UEFI mode.
@@ -438,13 +438,13 @@ Use the following commands:
         [`boot.loader.systemd-boot`](#opt-boot.loader.systemd-boot.enable)
         as well.
 
-    :   If you want to use GRUB, set [](#opt-boot.loader.grub.device) to `nodev` and
+        If you want to use GRUB, set [](#opt-boot.loader.grub.device) to `nodev` and
         [](#opt-boot.loader.grub.efiSupport) to `true`.
 
-    :   With system-boot, you should not need any special configuration to detect
+        With systemd-boot, you should not need any special configuration to detect
         other installed systems. With GRUB, set [](#opt-boot.loader.grub.useOSProber)
-        to `true`, but this will only detect windows partitions, not other linux
-        distributions. If you dual boot another linux distribution, use system-boot
+        to `true`, but this will only detect windows partitions, not other Linux
+        distributions. If you dual boot another Linux distribution, use systemd-boot
         instead.
 
     If you need to configure networking for your machine the
@@ -538,9 +538,7 @@ drive (here `/dev/sda`). [Example: NixOS Configuration](#ex-config) shows a
 corresponding configuration Nix expression.
 
 ::: {#ex-partition-scheme-MBR .example}
-::: {.title}
-**Example: Example partition schemes for NixOS on `/dev/sda` (MBR)**
-:::
+### Example partition schemes for NixOS on `/dev/sda` (MBR)
 ```ShellSession
 # parted /dev/sda -- mklabel msdos
 # parted /dev/sda -- mkpart primary 1MB -8GB
@@ -549,9 +547,7 @@ corresponding configuration Nix expression.
 :::
 
 ::: {#ex-partition-scheme-UEFI .example}
-::: {.title}
-**Example: Example partition schemes for NixOS on `/dev/sda` (UEFI)**
-:::
+### Example partition schemes for NixOS on `/dev/sda` (UEFI)
 ```ShellSession
 # parted /dev/sda -- mklabel gpt
 # parted /dev/sda -- mkpart primary 512MB -8GB
@@ -562,9 +558,8 @@ corresponding configuration Nix expression.
 :::
 
 ::: {#ex-install-sequence .example}
-::: {.title}
-**Example: Commands for Installing NixOS on `/dev/sda`**
-:::
+### Commands for Installing NixOS on `/dev/sda`
+
 With a partitioned disk.
 
 ```ShellSession
@@ -583,9 +578,7 @@ With a partitioned disk.
 :::
 
 ::: {#ex-config .example}
-::: {.title}
-**Example: NixOS Configuration**
-:::
+### Example: NixOS Configuration
 ```ShellSession
 { config, pkgs, ... }: {
   imports = [
@@ -609,11 +602,11 @@ With a partitioned disk.
 
 ## Additional installation notes {#sec-installation-additional-notes}
 
-```{=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" />
+```{=include=} sections
+installing-usb.section.md
+installing-pxe.section.md
+installing-kexec.section.md
+installing-virtualbox-guest.section.md
+installing-from-other-distro.section.md
+installing-behind-a-proxy.section.md
 ```
diff --git a/nixos/doc/manual/installation/upgrading.chapter.md b/nixos/doc/manual/installation/upgrading.chapter.md
index 249bcd97cec..26b6b8cc23e 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-22.11`](https://nixos.org/channels/nixos-22.05).
+-   *Stable channels*, such as [`nixos-22.11`](https://nixos.org/channels/nixos-22.11).
     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-22.11-small`](https://nixos.org/channels/nixos-22.05-small)
+-   *Small channels*, such as [`nixos-22.11-small`](https://nixos.org/channels/nixos-22.11-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
diff --git a/nixos/doc/manual/man-configuration.xml b/nixos/doc/manual/man-configuration.xml
deleted file mode 100644
index ddb1408fdcf..00000000000
--- a/nixos/doc/manual/man-configuration.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<refentry xmlns="http://docbook.org/ns/docbook"
-          xmlns:xlink="http://www.w3.org/1999/xlink"
-          xmlns:xi="http://www.w3.org/2001/XInclude">
- <refmeta>
-  <refentrytitle><filename>configuration.nix</filename>
-  </refentrytitle><manvolnum>5</manvolnum>
-  <refmiscinfo class="source">NixOS</refmiscinfo>
-<!-- <refmiscinfo class="version"><xi:include href="version.txt" parse="text"/></refmiscinfo> -->
- </refmeta>
- <refnamediv>
-  <refname><filename>configuration.nix</filename></refname>
-  <refpurpose>NixOS system configuration specification</refpurpose>
- </refnamediv>
- <refsection>
-  <title>Description</title>
-  <para>
-   The file <filename>/etc/nixos/configuration.nix</filename> contains the
-   declarative specification of your NixOS system configuration. The command
-   <command>nixos-rebuild</command> takes this file and realises the system
-   configuration specified therein.
-  </para>
- </refsection>
- <refsection>
-  <title>Options</title>
-  <para>
-   You can use the following options in <filename>configuration.nix</filename>.
-  </para>
-  <xi:include href="./generated/options-db.xml"
-            xpointer="configuration-variable-list" />
- </refsection>
-</refentry>
diff --git a/nixos/doc/manual/man-nixos-build-vms.xml b/nixos/doc/manual/man-nixos-build-vms.xml
deleted file mode 100644
index fa7c8c0c6d7..00000000000
--- a/nixos/doc/manual/man-nixos-build-vms.xml
+++ /dev/null
@@ -1,138 +0,0 @@
-<refentry xmlns="http://docbook.org/ns/docbook"
-          xmlns:xlink="http://www.w3.org/1999/xlink"
-          xmlns:xi="http://www.w3.org/2001/XInclude">
- <refmeta>
-  <refentrytitle><command>nixos-build-vms</command>
-  </refentrytitle><manvolnum>8</manvolnum>
-  <refmiscinfo class="source">NixOS</refmiscinfo>
-<!-- <refmiscinfo class="version"><xi:include href="version.txt" parse="text"/></refmiscinfo> -->
- </refmeta>
- <refnamediv>
-  <refname><command>nixos-build-vms</command></refname>
-  <refpurpose>build a network of virtual machines from a network of NixOS configurations</refpurpose>
- </refnamediv>
- <refsynopsisdiv>
-  <cmdsynopsis>
-   <command>nixos-build-vms</command>
-   <arg>
-    <option>--show-trace</option>
-   </arg>
-
-   <arg>
-    <option>--no-out-link</option>
-   </arg>
-
-   <arg>
-    <option>--help</option>
-  </arg>
-
-  <arg>
-    <option>--option</option>
-    <replaceable>name</replaceable>
-    <replaceable>value</replaceable>
-  </arg>
-
-   <arg choice="plain">
-    <replaceable>network.nix</replaceable>
-   </arg>
-  </cmdsynopsis>
- </refsynopsisdiv>
- <refsection>
-  <title>Description</title>
-  <para>
-   This command builds a network of QEMU-KVM virtual machines of a Nix
-   expression specifying a network of NixOS machines. The virtual network can
-   be started by executing the <filename>bin/run-vms</filename> shell script
-   that is generated by this command. By default, a <filename>result</filename>
-   symlink is produced that points to the generated virtual network.
-  </para>
-  <para>
-   A network Nix expression has the following structure:
-<screen>
-{
-  test1 = {pkgs, config, ...}:
-    {
-      services.openssh.enable = true;
-      nixpkgs.localSystem.system = "i686-linux";
-      deployment.targetHost = "test1.example.net";
-
-      # Other NixOS options
-    };
-
-  test2 = {pkgs, config, ...}:
-    {
-      services.openssh.enable = true;
-      services.httpd.enable = true;
-      environment.systemPackages = [ pkgs.lynx ];
-      nixpkgs.localSystem.system = "x86_64-linux";
-      deployment.targetHost = "test2.example.net";
-
-      # Other NixOS options
-    };
-}
-</screen>
-   Each attribute in the expression represents a machine in the network (e.g.
-   <varname>test1</varname> and <varname>test2</varname>) referring to a
-   function defining a NixOS configuration. In each NixOS configuration, two
-   attributes have a special meaning. The
-   <varname>deployment.targetHost</varname> specifies the address (domain name
-   or IP address) of the system which is used by <command>ssh</command> to
-   perform remote deployment operations. The
-   <varname>nixpkgs.localSystem.system</varname> attribute can be used to
-   specify an architecture for the target machine, such as
-   <varname>i686-linux</varname> which builds a 32-bit NixOS configuration.
-   Omitting this property will build the configuration for the same
-   architecture as the host system.
-  </para>
- </refsection>
- <refsection>
-  <title>Options</title>
-  <para>
-   This command accepts the following options:
-  </para>
-  <variablelist>
-   <varlistentry>
-    <term>
-     <option>--show-trace</option>
-    </term>
-    <listitem>
-     <para>
-      Shows a trace of the output.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--no-out-link</option>
-    </term>
-    <listitem>
-     <para>
-      Do not create a 'result' symlink.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>-h</option>, <option>--help</option>
-    </term>
-    <listitem>
-     <para>
-      Shows the usage of this command to the user.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--option</option> <replaceable>name</replaceable> <replaceable>value</replaceable>
-    </term>
-    <listitem>
-     <para>Set the Nix configuration option
-      <replaceable>name</replaceable> to <replaceable>value</replaceable>.
-      This overrides settings in the Nix configuration file (see
-      <citerefentry><refentrytitle>nix.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>).
-     </para>
-    </listitem>
-   </varlistentry>
-  </variablelist>
- </refsection>
-</refentry>
diff --git a/nixos/doc/manual/man-nixos-enter.xml b/nixos/doc/manual/man-nixos-enter.xml
deleted file mode 100644
index 41f0e6b9751..00000000000
--- a/nixos/doc/manual/man-nixos-enter.xml
+++ /dev/null
@@ -1,154 +0,0 @@
-<refentry xmlns="http://docbook.org/ns/docbook"
-          xmlns:xlink="http://www.w3.org/1999/xlink"
-          xmlns:xi="http://www.w3.org/2001/XInclude">
- <refmeta>
-  <refentrytitle><command>nixos-enter</command>
-  </refentrytitle><manvolnum>8</manvolnum>
-  <refmiscinfo class="source">NixOS</refmiscinfo>
-<!-- <refmiscinfo class="version"><xi:include href="version.txt" parse="text"/></refmiscinfo> -->
- </refmeta>
- <refnamediv>
-  <refname><command>nixos-enter</command></refname>
-  <refpurpose>run a command in a NixOS chroot environment</refpurpose>
- </refnamediv>
- <refsynopsisdiv>
-  <cmdsynopsis>
-   <command>nixos-enter</command>
-   <arg>
-    <arg choice='plain'>
-     <option>--root</option>
-    </arg>
-     <replaceable>root</replaceable>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--system</option>
-    </arg>
-     <replaceable>system</replaceable>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>-c</option>
-    </arg>
-     <replaceable>shell-command</replaceable>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--silent</option>
-    </arg>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--help</option>
-    </arg>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--</option>
-    </arg>
-     <replaceable>arguments</replaceable>
-   </arg>
-  </cmdsynopsis>
- </refsynopsisdiv>
- <refsection>
-  <title>Description</title>
-  <para>
-   This command runs a command in a NixOS chroot environment, that is, in a
-   filesystem hierarchy previously prepared using
-   <command>nixos-install</command>.
-  </para>
- </refsection>
- <refsection>
-  <title>Options</title>
-  <para>
-   This command accepts the following options:
-  </para>
-  <variablelist>
-   <varlistentry>
-    <term>
-     <option>--root</option>
-    </term>
-    <listitem>
-     <para>
-      The path to the NixOS system you want to enter. It defaults to
-      <filename>/mnt</filename>.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--system</option>
-    </term>
-    <listitem>
-     <para>
-      The NixOS system configuration to use. It defaults to
-      <filename>/nix/var/nix/profiles/system</filename>. You can enter a
-      previous NixOS configuration by specifying a path such as
-      <filename>/nix/var/nix/profiles/system-106-link</filename>.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--command</option>
-    </term>
-    <term>
-     <option>-c</option>
-    </term>
-    <listitem>
-     <para>
-      The bash command to execute.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--silent</option>
-    </term>
-    <listitem>
-     <para>
-       Suppresses all output from the activation script of the target system.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--</option>
-    </term>
-    <listitem>
-     <para>
-      Interpret the remaining arguments as the program name and arguments to be
-      invoked. The program is not executed in a shell.
-     </para>
-    </listitem>
-   </varlistentry>
-  </variablelist>
- </refsection>
- <refsection>
-  <title>Examples</title>
-  <para>
-   Start an interactive shell in the NixOS installation in
-   <filename>/mnt</filename>:
-  </para>
-<screen>
-<prompt># </prompt>nixos-enter --root /mnt
-</screen>
-  <para>
-   Run a shell command:
-  </para>
-<screen>
-<prompt># </prompt>nixos-enter -c 'ls -l /; cat /proc/mounts'
-</screen>
-  <para>
-   Run a non-shell command:
-  </para>
-<screen>
-# nixos-enter -- cat /proc/mounts
-</screen>
- </refsection>
-</refentry>
diff --git a/nixos/doc/manual/man-nixos-generate-config.xml b/nixos/doc/manual/man-nixos-generate-config.xml
deleted file mode 100644
index 9ac3b918ff6..00000000000
--- a/nixos/doc/manual/man-nixos-generate-config.xml
+++ /dev/null
@@ -1,214 +0,0 @@
-<refentry xmlns="http://docbook.org/ns/docbook"
-          xmlns:xlink="http://www.w3.org/1999/xlink"
-          xmlns:xi="http://www.w3.org/2001/XInclude">
- <refmeta>
-  <refentrytitle><command>nixos-generate-config</command>
-  </refentrytitle><manvolnum>8</manvolnum>
-  <refmiscinfo class="source">NixOS</refmiscinfo>
-<!-- <refmiscinfo class="version"><xi:include href="version.txt" parse="text"/></refmiscinfo> -->
- </refmeta>
- <refnamediv>
-  <refname><command>nixos-generate-config</command></refname>
-  <refpurpose>generate NixOS configuration modules</refpurpose>
- </refnamediv>
- <refsynopsisdiv>
-  <cmdsynopsis>
-   <command>nixos-generate-config</command>
-   <arg>
-    <option>--force</option>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--root</option>
-    </arg>
-     <replaceable>root</replaceable>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--dir</option>
-    </arg>
-     <replaceable>dir</replaceable>
-   </arg>
-  </cmdsynopsis>
- </refsynopsisdiv>
- <refsection>
-  <title>Description</title>
-  <para>
-   This command writes two NixOS configuration modules:
-   <variablelist>
-    <varlistentry>
-     <term>
-      <option>/etc/nixos/hardware-configuration.nix</option>
-     </term>
-     <listitem>
-      <para>
-       This module sets NixOS configuration options based on your current
-       hardware configuration. In particular, it sets the
-       <option>fileSystem</option> option to reflect all currently mounted file
-       systems, the <option>swapDevices</option> option to reflect active swap
-       devices, and the <option>boot.initrd.*</option> options to ensure that
-       the initial ramdisk contains any kernel modules necessary for mounting
-       the root file system.
-      </para>
-      <para>
-       If this file already exists, it is overwritten. Thus, you should not
-       modify it manually. Rather, you should include it from your
-       <filename>/etc/nixos/configuration.nix</filename>, and re-run
-       <command>nixos-generate-config</command> to update it whenever your
-       hardware configuration changes.
-      </para>
-     </listitem>
-    </varlistentry>
-    <varlistentry>
-     <term>
-      <option>/etc/nixos/configuration.nix</option>
-     </term>
-     <listitem>
-      <para>
-       This is the main NixOS system configuration module. If it already
-       exists, it’s left unchanged. Otherwise,
-       <command>nixos-generate-config</command> will write a template for you
-       to customise.
-      </para>
-     </listitem>
-    </varlistentry>
-   </variablelist>
-  </para>
- </refsection>
- <refsection>
-  <title>Options</title>
-  <para>
-   This command accepts the following options:
-  </para>
-  <variablelist>
-   <varlistentry>
-    <term>
-     <option>--root</option>
-    </term>
-    <listitem>
-     <para>
-      If this option is given, treat the directory
-      <replaceable>root</replaceable> as the root of the file system. This
-      means that configuration files will be written to
-      <filename><replaceable>root</replaceable>/etc/nixos</filename>, and that
-      any file systems outside of <replaceable>root</replaceable> are ignored
-      for the purpose of generating the <option>fileSystems</option> option.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--dir</option>
-    </term>
-    <listitem>
-     <para>
-      If this option is given, write the configuration files to the directory
-      <replaceable>dir</replaceable> instead of
-      <filename>/etc/nixos</filename>.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--force</option>
-    </term>
-    <listitem>
-     <para>
-      Overwrite <filename>/etc/nixos/configuration.nix</filename> if it already
-      exists.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--no-filesystems</option>
-    </term>
-    <listitem>
-     <para>
-      Omit everything concerning file systems and swap devices from the
-      hardware configuration.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--show-hardware-config</option>
-    </term>
-    <listitem>
-     <para>
-      Don't generate <filename>configuration.nix</filename> or
-      <filename>hardware-configuration.nix</filename> and print the hardware
-      configuration to stdout only.
-     </para>
-    </listitem>
-   </varlistentry>
-  </variablelist>
- </refsection>
- <refsection>
-  <title>Examples</title>
-  <para>
-   This command is typically used during NixOS installation to write initial
-   configuration modules. For example, if you created and mounted the target
-   file systems on <filename>/mnt</filename> and
-   <filename>/mnt/boot</filename>, you would run:
-<screen>
-<prompt>$ </prompt>nixos-generate-config --root /mnt
-</screen>
-   The resulting file
-   <filename>/mnt/etc/nixos/hardware-configuration.nix</filename> might look
-   like this:
-<programlisting>
-# Do not modify this file!  It was generated by ‘nixos-generate-config’
-# and may be overwritten by future invocations.  Please make changes
-# to /etc/nixos/configuration.nix instead.
-{ config, pkgs, ... }:
-
-{
-  imports =
-    [ &lt;nixos/modules/installer/scan/not-detected.nix&gt;
-    ];
-
-  boot.initrd.availableKernelModules = [ "ehci_hcd" "ahci" ];
-  boot.kernelModules = [ "kvm-intel" ];
-  boot.extraModulePackages = [ ];
-
-  fileSystems."/" =
-    { device = "/dev/disk/by-label/nixos";
-      fsType = "ext3";
-      options = [ "rw" "data=ordered" "relatime" ];
-    };
-
-  fileSystems."/boot" =
-    { device = "/dev/sda1";
-      fsType = "ext3";
-      options = [ "rw" "errors=continue" "user_xattr" "acl" "barrier=1" "data=writeback" "relatime" ];
-    };
-
-  swapDevices =
-    [ { device = "/dev/sda2"; }
-    ];
-
-  nix.maxJobs = 8;
-}
-</programlisting>
-   It will also create a basic
-   <filename>/mnt/etc/nixos/configuration.nix</filename>, which you should edit
-   to customise the logical configuration of your system. This file includes
-   the result of the hardware scan as follows:
-<programlisting>
-  imports = [ ./hardware-configuration.nix ];
-</programlisting>
-  </para>
-  <para>
-   After installation, if your hardware configuration changes, you can run:
-<screen>
-<prompt>$ </prompt>nixos-generate-config
-</screen>
-   to update <filename>/etc/nixos/hardware-configuration.nix</filename>. Your
-   <filename>/etc/nixos/configuration.nix</filename> will
-   <emphasis>not</emphasis> be overwritten.
-  </para>
- </refsection>
-</refentry>
diff --git a/nixos/doc/manual/man-nixos-install.xml b/nixos/doc/manual/man-nixos-install.xml
deleted file mode 100644
index eb6680b6567..00000000000
--- a/nixos/doc/manual/man-nixos-install.xml
+++ /dev/null
@@ -1,357 +0,0 @@
-<refentry xmlns="http://docbook.org/ns/docbook"
-          xmlns:xlink="http://www.w3.org/1999/xlink"
-          xmlns:xi="http://www.w3.org/2001/XInclude">
- <refmeta>
-  <refentrytitle><command>nixos-install</command>
-  </refentrytitle><manvolnum>8</manvolnum>
-  <refmiscinfo class="source">NixOS</refmiscinfo>
-<!-- <refmiscinfo class="version"><xi:include href="version.txt" parse="text"/></refmiscinfo> -->
- </refmeta>
- <refnamediv>
-  <refname><command>nixos-install</command></refname>
-  <refpurpose>install bootloader and NixOS</refpurpose>
- </refnamediv>
- <refsynopsisdiv>
-  <cmdsynopsis>
-   <command>nixos-install</command>
-   <arg>
-    <group choice='req'>
-     <arg choice='plain'>
-      <option>--verbose</option>
-     </arg>
-     <arg choice='plain'>
-      <option>-v</option>
-     </arg>
-    </group>
-   </arg>
-   <arg>
-    <arg choice='plain'>
-     <option>-I</option>
-    </arg>
-     <replaceable>path</replaceable>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--root</option>
-    </arg>
-     <replaceable>root</replaceable>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--system</option>
-    </arg>
-     <replaceable>path</replaceable>
-   </arg>
-
-   <arg>
-    <option>--flake</option> <replaceable>flake-uri</replaceable>
-   </arg>
-
-   <arg>
-    <group choice='req'>
-     <arg choice='plain'><option>--impure</option></arg>
-    </group>
-   </arg>
-
-   <arg>
-     <arg choice='plain'>
-       <option>--channel</option>
-     </arg>
-     <replaceable>channel</replaceable>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--no-channel-copy</option>
-    </arg>
-   </arg>
-
-   <arg>
-    <group choice='req'>
-     <arg choice='plain'>
-      <option>--no-root-password</option>
-     </arg>
-     <arg choice='plain'>
-      <option>--no-root-passwd</option>
-     </arg>
-    </group>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--no-bootloader</option>
-    </arg>
-   </arg>
-
-   <arg>
-    <group choice='req'>
-    <arg choice='plain'>
-     <option>--max-jobs</option>
-    </arg>
-
-    <arg choice='plain'>
-     <option>-j</option>
-    </arg>
-     </group> <replaceable>number</replaceable>
-   </arg>
-
-   <arg>
-    <option>--cores</option> <replaceable>number</replaceable>
-   </arg>
-
-   <arg>
-    <option>--option</option> <replaceable>name</replaceable> <replaceable>value</replaceable>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--show-trace</option>
-    </arg>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--keep-going</option>
-    </arg>
-   </arg>
-
-   <arg>
-    <arg choice='plain'>
-     <option>--help</option>
-    </arg>
-   </arg>
-  </cmdsynopsis>
- </refsynopsisdiv>
- <refsection>
-  <title>Description</title>
-  <para>
-   This command installs NixOS in the file system mounted on
-   <filename>/mnt</filename>, based on the NixOS configuration specified in
-   <filename>/mnt/etc/nixos/configuration.nix</filename>. It performs the
-   following steps:
-   <itemizedlist>
-    <listitem>
-     <para>
-      It copies Nix and its dependencies to
-      <filename>/mnt/nix/store</filename>.
-     </para>
-    </listitem>
-    <listitem>
-     <para>
-      It runs Nix in <filename>/mnt</filename> to build the NixOS configuration
-      specified in <filename>/mnt/etc/nixos/configuration.nix</filename>.
-     </para>
-    </listitem>
-    <listitem>
-      <para>
-        It installs the current channel <quote>nixos</quote> in the target channel
-        profile (unless <option>--no-channel-copy</option> is specified).
-      </para>
-    </listitem>
-    <listitem>
-     <para>
-      It installs the GRUB boot loader on the device specified in the option
-      <option>boot.loader.grub.device</option> (unless
-      <option>--no-bootloader</option> is specified), and generates a GRUB
-      configuration file that boots into the NixOS configuration just
-      installed.
-     </para>
-    </listitem>
-    <listitem>
-     <para>
-      It prompts you for a password for the root account (unless
-      <option>--no-root-password</option> is specified).
-     </para>
-    </listitem>
-   </itemizedlist>
-  </para>
-  <para>
-   This command is idempotent: if it is interrupted or fails due to a temporary
-   problem (e.g. a network issue), you can safely re-run it.
-  </para>
- </refsection>
- <refsection>
-  <title>Options</title>
-  <para>
-   This command accepts the following options:
-  </para>
-  <variablelist>
-   <varlistentry>
-    <term><option>--verbose</option> / <option>-v</option></term>
-    <listitem>
-     <para>Increases the level of verbosity of diagnostic messages
-     printed on standard error.  For each Nix operation, the information
-     printed on standard output is well-defined; any diagnostic
-     information is printed on standard error, never on standard
-     output.</para>
-     <para>Please note that this option may be specified repeatedly.</para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--root</option>
-    </term>
-    <listitem>
-     <para>
-      Defaults to <filename>/mnt</filename>. If this option is given, treat the
-      directory <replaceable>root</replaceable> as the root of the NixOS
-      installation.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--system</option>
-    </term>
-    <listitem>
-     <para>
-      If this option is provided, <command>nixos-install</command> will install
-      the specified closure rather than attempt to build one from
-      <filename>/mnt/etc/nixos/configuration.nix</filename>.
-     </para>
-     <para>
-      The closure must be an appropriately configured NixOS system, with boot
-      loader and partition configuration that fits the target host. Such a
-      closure is typically obtained with a command such as <command>nix-build
-      -I nixos-config=./configuration.nix '&lt;nixpkgs/nixos&gt;' -A system
-      --no-out-link</command>
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--flake</option> <replaceable>flake-uri</replaceable>#<replaceable>name</replaceable>
-    </term>
-    <listitem>
-     <para>
-      Build the NixOS system from the specified flake.
-      The flake must contain an output named
-      <literal>nixosConfigurations.<replaceable>name</replaceable></literal>.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-     <term>
-       <option>--channel</option>
-     </term>
-     <listitem>
-       <para>
-         If this option is provided, do not copy the current
-         <quote>nixos</quote> channel to the target host. Instead, use the
-         specified derivation.
-       </para>
-     </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>-I</option>
-    </term>
-    <listitem>
-     <para>
-      Add a path to the Nix expression search path. This option may be given
-      multiple times. See the NIX_PATH environment variable for information on
-      the semantics of the Nix search path. Paths added through
-      <replaceable>-I</replaceable> take precedence over NIX_PATH.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--max-jobs</option>
-    </term>
-    <term>
-     <option>-j</option>
-    </term>
-    <listitem>
-     <para>
-      Sets the maximum number of build jobs that Nix will perform in parallel
-      to the specified number. The default is <literal>1</literal>. A higher
-      value is useful on SMP systems or to exploit I/O latency.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--cores</option>
-    </term>
-    <listitem>
-     <para>
-      Sets the value of the <envar>NIX_BUILD_CORES</envar> environment variable
-      in the invocation of builders. Builders can use this variable at their
-      discretion to control the maximum amount of parallelism. For instance, in
-      Nixpkgs, if the derivation attribute
-      <varname>enableParallelBuilding</varname> is set to
-      <literal>true</literal>, the builder passes the
-      <option>-j<replaceable>N</replaceable></option> flag to GNU Make. The
-      value <literal>0</literal> means that the builder should use all
-      available CPU cores in the system.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--option</option> <replaceable>name</replaceable> <replaceable>value</replaceable>
-    </term>
-    <listitem>
-     <para>
-      Set the Nix configuration option <replaceable>name</replaceable> to
-      <replaceable>value</replaceable>.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--show-trace</option>
-    </term>
-    <listitem>
-     <para>
-      Causes Nix to print out a stack trace in case of Nix expression
-      evaluation errors.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--keep-going</option>
-    </term>
-    <listitem>
-     <para>
-      Causes Nix to continue building derivations as far as possible
-      in the face of failed builds.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>--help</option>
-    </term>
-    <listitem>
-     <para>
-      Synonym for <command>man nixos-install</command>.
-     </para>
-    </listitem>
-   </varlistentry>
-  </variablelist>
- </refsection>
- <refsection>
-  <title>Examples</title>
-  <para>
-   A typical NixOS installation is done by creating and mounting a file system
-   on <filename>/mnt</filename>, generating a NixOS configuration in
-   <filename>/mnt/etc/nixos/configuration.nix</filename>, and running
-   <command>nixos-install</command>. For instance, if we want to install NixOS
-   on an <literal>ext4</literal> file system created in
-   <filename>/dev/sda1</filename>:
-<screen>
-<prompt>$ </prompt>mkfs.ext4 /dev/sda1
-<prompt>$ </prompt>mount /dev/sda1 /mnt
-<prompt>$ </prompt>nixos-generate-config --root /mnt
-<prompt>$ </prompt># edit /mnt/etc/nixos/configuration.nix
-<prompt>$ </prompt>nixos-install
-<prompt>$ </prompt>reboot
-</screen>
-  </para>
- </refsection>
-</refentry>
diff --git a/nixos/doc/manual/man-nixos-option.xml b/nixos/doc/manual/man-nixos-option.xml
deleted file mode 100644
index b921386d0df..00000000000
--- a/nixos/doc/manual/man-nixos-option.xml
+++ /dev/null
@@ -1,134 +0,0 @@
-<refentry xmlns="http://docbook.org/ns/docbook"
-          xmlns:xlink="http://www.w3.org/1999/xlink"
-          xmlns:xi="http://www.w3.org/2001/XInclude">
- <refmeta>
-  <refentrytitle><command>nixos-option</command>
-  </refentrytitle><manvolnum>8</manvolnum>
-  <refmiscinfo class="source">NixOS</refmiscinfo>
-<!-- <refmiscinfo class="version"><xi:include href="version.txt" parse="text"/></refmiscinfo> -->
- </refmeta>
- <refnamediv>
-  <refname><command>nixos-option</command></refname>
-  <refpurpose>inspect a NixOS configuration</refpurpose>
- </refnamediv>
- <refsynopsisdiv>
-  <cmdsynopsis>
-   <command>nixos-option</command>
-
-   <arg>
-    <group choice='req'>
-     <arg choice='plain'><option>-r</option></arg>
-     <arg choice='plain'><option>--recursive</option></arg>
-    </group>
-   </arg>
-
-   <arg>
-    <option>-I</option> <replaceable>path</replaceable>
-   </arg>
-
-   <arg>
-    <replaceable>option.name</replaceable>
-   </arg>
-  </cmdsynopsis>
- </refsynopsisdiv>
- <refsection>
-  <title>Description</title>
-  <para>
-   This command evaluates the configuration specified in
-   <filename>/etc/nixos/configuration.nix</filename> and returns the properties
-   of the option name given as argument.
-  </para>
-  <para>
-   When the option name is not an option, the command prints the list of
-   attributes contained in the attribute set.
-  </para>
- </refsection>
- <refsection>
-  <title>Options</title>
-  <para>
-   This command accepts the following options:
-  </para>
-  <variablelist>
-   <varlistentry>
-    <term><option>-r</option></term>
-    <term><option>--recursive</option></term>
-    <listitem>
-     <para>
-      Print all the values at or below the specified path recursively.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry>
-    <term>
-     <option>-I</option> <replaceable>path</replaceable>
-    </term>
-    <listitem>
-     <para>
-      This option is passed to the underlying
-      <command>nix-instantiate</command> invocation.
-     </para>
-    </listitem>
-   </varlistentry>
-  </variablelist>
- </refsection>
- <refsection>
-  <title>Environment</title>
-  <variablelist>
-   <varlistentry>
-    <term>
-     <envar>NIXOS_CONFIG</envar>
-    </term>
-    <listitem>
-     <para>
-      Path to the main NixOS configuration module. Defaults to
-      <filename>/etc/nixos/configuration.nix</filename>.
-     </para>
-    </listitem>
-   </varlistentry>
-  </variablelist>
- </refsection>
- <refsection>
-  <title>Examples</title>
-  <para>
-   Investigate option values:
-<screen><prompt>$ </prompt>nixos-option boot.loader
-This attribute set contains:
-generationsDir
-grub
-initScript
-
-<prompt>$ </prompt>nixos-option boot.loader.grub.enable
-Value:
-true
-
-Default:
-true
-
-Description:
-Whether to enable the GNU GRUB boot loader.
-
-Declared by:
-  "/nix/var/nix/profiles/per-user/root/channels/nixos/nixpkgs/nixos/modules/system/boot/loader/grub/grub.nix"
-
-Defined by:
-  "/nix/var/nix/profiles/per-user/root/channels/nixos/nixpkgs/nixos/modules/system/boot/loader/grub/grub.nix"
-</screen>
-  </para>
- </refsection>
- <refsection>
-  <title>Bugs</title>
-  <para>
-   The author listed in the following section is wrong. If there is any other
-   bug, please report to Nicolas Pierron.
-  </para>
- </refsection>
- <refsection>
-  <title>See also</title>
-  <para>
-   <citerefentry>
-    <refentrytitle>configuration.nix</refentrytitle>
-    <manvolnum>5</manvolnum>
-   </citerefentry>
-  </para>
- </refsection>
-</refentry>
diff --git a/nixos/doc/manual/man-nixos-rebuild.xml b/nixos/doc/manual/man-nixos-rebuild.xml
deleted file mode 100644
index cab871661a7..00000000000
--- a/nixos/doc/manual/man-nixos-rebuild.xml
+++ /dev/null
@@ -1,730 +0,0 @@
-<refentry xmlns="http://docbook.org/ns/docbook"
-          xmlns:xlink="http://www.w3.org/1999/xlink"
-          xmlns:xi="http://www.w3.org/2001/XInclude">
- <refmeta>
-  <refentrytitle><command>nixos-rebuild</command>
-  </refentrytitle><manvolnum>8</manvolnum>
-  <refmiscinfo class="source">NixOS</refmiscinfo>
-<!-- <refmiscinfo class="version"><xi:include href="version.txt" parse="text"/></refmiscinfo> -->
- </refmeta>
-
- <refnamediv>
-  <refname><command>nixos-rebuild</command></refname>
-  <refpurpose>reconfigure a NixOS machine</refpurpose>
- </refnamediv>
-
- <refsynopsisdiv>
-  <cmdsynopsis>
-   <command>nixos-rebuild</command><group choice='req'>
-   <arg choice='plain'>
-    <option>switch</option>
-   </arg>
-
-   <arg choice='plain'>
-    <option>boot</option>
-   </arg>
-
-   <arg choice='plain'>
-    <option>test</option>
-   </arg>
-
-   <arg choice='plain'>
-    <option>build</option>
-   </arg>
-
-   <arg choice='plain'>
-    <option>dry-build</option>
-   </arg>
-
-   <arg choice='plain'>
-    <option>dry-activate</option>
-   </arg>
-
-   <arg choice='plain'>
-    <option>edit</option>
-   </arg>
-
-   <arg choice='plain'>
-    <option>build-vm</option>
-   </arg>
-
-   <arg choice='plain'>
-    <option>build-vm-with-bootloader</option>
-   </arg>
-    </group>
-    <sbr />
-
-    <arg>
-      <group choice='req'>
-        <arg choice='plain'>
-          <option>--upgrade</option>
-        </arg>
-        <arg choice='plain'>
-          <option>--upgrade-all</option>
-        </arg>
-      </group>
-    </arg>
-
-   <arg>
-    <option>--install-bootloader</option>
-   </arg>
-
-   <arg>
-    <option>--no-build-nix</option>
-   </arg>
-
-   <arg>
-    <option>--fast</option>
-   </arg>
-
-   <arg>
-    <option>--rollback</option>
-   </arg>
-
-   <arg>
-    <option>--builders</option> <replaceable>builder-spec</replaceable>
-   </arg>
-
-   <sbr/>
-
-   <arg>
-    <option>--flake</option> <replaceable>flake-uri</replaceable>
-   </arg>
-
-   <arg>
-    <option>--no-flake</option>
-   </arg>
-
-   <arg>
-    <option>--override-input</option> <replaceable>input-name</replaceable> <replaceable>flake-uri</replaceable>
-   </arg>
-
-   <sbr />
-
-   <arg>
-    <group choice='req'>
-    <arg choice='plain'>
-     <option>--profile-name</option>
-    </arg>
-
-    <arg choice='plain'>
-     <option>-p</option>
-    </arg>
-     </group> <replaceable>name</replaceable>
-   </arg>
-
-   <sbr />
-
-   <arg>
-    <option>--build-host</option> <replaceable>host</replaceable>
-   </arg>
-
-   <arg>
-    <option>--target-host</option> <replaceable>host</replaceable>
-   </arg>
-
-   <arg>
-    <option>--use-remote-sudo</option>
-   </arg>
-
-   <sbr />
-
-   <arg>
-    <option>--show-trace</option>
-   </arg>
-   <arg>
-    <option>-I</option>
-    <replaceable>NIX_PATH</replaceable>
-   </arg>
-   <arg>
-    <group choice='req'>
-     <arg choice='plain'><option>--verbose</option></arg>
-     <arg choice='plain'><option>-v</option></arg>
-    </group>
-   </arg>
-   <arg>
-    <group choice='req'>
-     <arg choice='plain'><option>--impure</option></arg>
-    </group>
-   </arg>
-   <arg>
-    <group choice='req'>
-     <arg choice='plain'><option>--max-jobs</option></arg>
-     <arg choice='plain'><option>-j</option></arg>
-    </group>
-    <replaceable>number</replaceable>
-   </arg>
-   <arg>
-    <group choice='req'>
-     <arg choice='plain'><option>--keep-failed</option></arg>
-     <arg choice='plain'><option>-K</option></arg>
-    </group>
-   </arg>
-   <arg>
-    <group choice='req'>
-     <arg choice='plain'><option>--keep-going</option></arg>
-     <arg choice='plain'><option>-k</option></arg>
-    </group>
-   </arg>
-  </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsection>
-  <title>Description</title>
-
-  <para>
-   This command updates the system so that it corresponds to the
-   configuration specified in
-   <filename>/etc/nixos/configuration.nix</filename> or
-   <filename>/etc/nixos/flake.nix</filename>. Thus, every time you
-   modify the configuration or any other NixOS module, you must run
-   <command>nixos-rebuild</command> to make the changes take
-   effect. It builds the new system in
-   <filename>/nix/store</filename>, runs its activation script, and
-   stop and (re)starts any system services if needed. Please note that
-   user services need to be started manually as they aren't detected
-   by the activation script at the moment.
-  </para>
-
-  <para>
-   This command has one required argument, which specifies the desired
-   operation. It must be one of the following:
-
-   <variablelist>
-    <varlistentry>
-     <term>
-      <option>switch</option>
-     </term>
-     <listitem>
-      <para>
-       Build and activate the new configuration, and make it the boot default.
-       That is, the configuration is added to the GRUB boot menu as the default
-       menu entry, so that subsequent reboots will boot the system into the new
-       configuration. Previous configurations activated with
-       <command>nixos-rebuild switch</command> or <command>nixos-rebuild
-       boot</command> remain available in the GRUB menu.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term>
-      <option>boot</option>
-     </term>
-     <listitem>
-      <para>
-       Build the new configuration and make it the boot default (as with
-       <command>nixos-rebuild switch</command>), but do not activate it. That
-       is, the system continues to run the previous configuration until the
-       next reboot.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term>
-      <option>test</option>
-     </term>
-     <listitem>
-      <para>
-       Build and activate the new configuration, but do not add it to the GRUB
-       boot menu. Thus, if you reboot the system (or if it crashes), you will
-       automatically revert to the default configuration (i.e. the
-       configuration resulting from the last call to <command>nixos-rebuild
-       switch</command> or <command>nixos-rebuild boot</command>).
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term>
-      <option>build</option>
-     </term>
-     <listitem>
-      <para>
-       Build the new configuration, but neither activate it nor add it to the
-       GRUB boot menu. It leaves a symlink named <filename>result</filename> in
-       the current directory, which points to the output of the top-level
-       “system” derivation. This is essentially the same as doing
-<screen>
-<prompt>$ </prompt>nix-build /path/to/nixpkgs/nixos -A system
-</screen>
-       Note that you do not need to be <literal>root</literal> to run
-       <command>nixos-rebuild build</command>.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term>
-      <option>dry-build</option>
-     </term>
-     <listitem>
-      <para>
-       Show what store paths would be built or downloaded by any of the
-       operations above, but otherwise do nothing.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term>
-      <option>dry-activate</option>
-     </term>
-     <listitem>
-      <para>
-       Build the new configuration, but instead of activating it, show what
-       changes would be performed by the activation (i.e. by
-       <command>nixos-rebuild test</command>). For instance, this command will
-       print which systemd units would be restarted. The list of changes is not
-       guaranteed to be complete.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term>
-      <option>edit</option>
-     </term>
-     <listitem>
-      <para>
-       Opens <filename>configuration.nix</filename> in the default editor.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term>
-      <option>build-vm</option>
-     </term>
-     <listitem>
-      <para>
-       Build a script that starts a NixOS virtual machine with the desired
-       configuration. It leaves a symlink <filename>result</filename> in the
-       current directory that points (under
-       <filename>result/bin/run-<replaceable>hostname</replaceable>-vm</filename>)
-       at the script that starts the VM. Thus, to test a NixOS configuration in
-       a virtual machine, you should do the following:
-<screen>
-<prompt>$ </prompt>nixos-rebuild build-vm
-<prompt>$ </prompt>./result/bin/run-*-vm
-</screen>
-      </para>
-
-      <para>
-       The VM is implemented using the <literal>qemu</literal> package. For
-       best performance, you should load the <literal>kvm-intel</literal> or
-       <literal>kvm-amd</literal> kernel modules to get hardware
-       virtualisation.
-      </para>
-
-      <para>
-       The VM mounts the Nix store of the host through the 9P file system. The
-       host Nix store is read-only, so Nix commands that modify the Nix store
-       will not work in the VM. This includes commands such as
-       <command>nixos-rebuild</command>; to change the VM’s configuration,
-       you must halt the VM and re-run the commands above.
-      </para>
-
-      <para>
-       The VM has its own <literal>ext3</literal> root file system, which is
-       automatically created when the VM is first started, and is persistent
-       across reboots of the VM. It is stored in
-       <literal>./<replaceable>hostname</replaceable>.qcow2</literal>.
-<!-- The entire file system hierarchy of the host is available in
-      the VM under <filename>/hostfs</filename>.-->
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term>
-      <option>build-vm-with-bootloader</option>
-     </term>
-     <listitem>
-      <para>
-       Like <option>build-vm</option>, but boots using the regular boot loader
-       of your configuration (e.g., GRUB 1 or 2), rather than booting directly
-       into the kernel and initial ramdisk of the system. This allows you to
-       test whether the boot loader works correctly. However, it does not
-       guarantee that your NixOS configuration will boot successfully on the
-       host hardware (i.e., after running <command>nixos-rebuild
-       switch</command>), because the hardware and boot loader configuration in
-       the VM are different. The boot loader is installed on an automatically
-       generated virtual disk containing a <filename>/boot</filename>
-       partition.
-      </para>
-     </listitem>
-    </varlistentry>
-   </variablelist>
-  </para>
- </refsection>
-
- <refsection>
-  <title>Options</title>
-  <para>
-   This command accepts the following options:
-  </para>
-
-  <variablelist>
-   <varlistentry>
-    <term>
-     <option>--upgrade</option>
-    </term>
-    <term>
-     <option>--upgrade-all</option>
-    </term>
-    <listitem>
-      <para>
-        Update the root user's channel named <literal>nixos</literal>
-        before rebuilding the system.
-      </para>
-      <para>
-        In addition to the <literal>nixos</literal> channel, the root
-        user's channels which have a file named
-        <literal>.update-on-nixos-rebuild</literal> in their base
-        directory will also be updated.
-      </para>
-      <para>
-        Passing <option>--upgrade-all</option> updates all of the root
-        user's channels.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--install-bootloader</option>
-    </term>
-    <listitem>
-     <para>
-      Causes the boot loader to be (re)installed on the device specified by the
-      relevant configuration options.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--no-build-nix</option>
-    </term>
-    <listitem>
-     <para>
-      Normally, <command>nixos-rebuild</command> first builds the
-      <varname>nixUnstable</varname> attribute in Nixpkgs, and uses the
-      resulting instance of the Nix package manager to build the new system
-      configuration. This is necessary if the NixOS modules use features not
-      provided by the currently installed version of Nix. This option disables
-      building a new Nix.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--fast</option>
-    </term>
-    <listitem>
-     <para>
-      Equivalent to <option>--no-build-nix</option>. This option is
-      useful if you call <command>nixos-rebuild</command> frequently
-      (e.g. if you’re hacking on a NixOS module).
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--rollback</option>
-    </term>
-    <listitem>
-     <para>
-      Instead of building a new configuration as specified by
-      <filename>/etc/nixos/configuration.nix</filename>, roll back to the
-      previous configuration. (The previous configuration is defined as the one
-      before the “current” generation of the Nix profile
-      <filename>/nix/var/nix/profiles/system</filename>.)
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--builders</option> <replaceable>builder-spec</replaceable>
-    </term>
-    <listitem>
-     <para>
-      Allow ad-hoc remote builders for building the new system. This requires
-      the user executing <command>nixos-rebuild</command> (usually root) to be
-      configured as a trusted user in the Nix daemon. This can be achieved by
-      using the <literal>nix.settings.trusted-users</literal> NixOS option. Examples
-      values for that option are described in the <literal>Remote builds
-      chapter</literal> in the Nix manual, (i.e. <command>--builders
-      "ssh://bigbrother x86_64-linux"</command>). By specifying an empty string
-      existing builders specified in <filename>/etc/nix/machines</filename> can
-      be ignored: <command>--builders ""</command> for example when they are
-      not reachable due to network connectivity.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--profile-name</option>
-    </term>
-    <term>
-     <option>-p</option>
-    </term>
-    <listitem>
-     <para>
-      Instead of using the Nix profile
-      <filename>/nix/var/nix/profiles/system</filename> to keep track of the
-      current and previous system configurations, use
-      <filename>/nix/var/nix/profiles/system-profiles/<replaceable>name</replaceable></filename>.
-      When you use GRUB 2, for every system profile created with this flag,
-      NixOS will create a submenu named “NixOS - Profile
-      '<replaceable>name</replaceable>'” in GRUB’s boot menu, containing
-      the current and previous configurations of this profile.
-     </para>
-     <para>
-      For instance, if you want to test a configuration file named
-      <filename>test.nix</filename> without affecting the default system
-      profile, you would do:
-<screen>
-<prompt>$ </prompt>nixos-rebuild switch -p test -I nixos-config=./test.nix
-</screen>
-      The new configuration will appear in the GRUB 2 submenu “NixOS -
-      Profile 'test'”.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--build-host</option>
-    </term>
-    <listitem>
-     <para>
-      Instead of building the new configuration locally, use the specified host
-      to perform the build. The host needs to be accessible with ssh, and must
-      be able to perform Nix builds. If the option
-      <option>--target-host</option> is not set, the build will be copied back
-      to the local machine when done.
-     </para>
-     <para>
-      Note that, if <option>--no-build-nix</option> is not specified, Nix will
-      be built both locally and remotely. This is because the configuration
-      will always be evaluated locally even though the building might be
-      performed remotely.
-     </para>
-     <para>
-      You can include a remote user name in the host name
-      (<replaceable>user@host</replaceable>). You can also set ssh options by
-      defining the <envar>NIX_SSHOPTS</envar> environment variable.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--target-host</option>
-    </term>
-    <listitem>
-     <para>
-      Specifies the NixOS target host. By setting this to something other than
-      <replaceable>localhost</replaceable>, the system activation will happen
-      on the remote host instead of the local machine. The remote host needs to
-      be accessible over ssh, and for the commands <option>switch</option>,
-      <option>boot</option> and <option>test</option> you need root access.
-     </para>
-
-     <para>
-      If <option>--build-host</option> is not explicitly specified, building
-      will take place locally.
-     </para>
-
-     <para>
-      You can include a remote user name in the host name
-      (<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>
-
-   <varlistentry>
-    <term>
-     <option>--use-substitutes</option>
-    </term>
-    <listitem>
-     <para>
-       When set, nixos-rebuild will add <option>--use-substitutes</option>
-       to each invocation of nix-copy-closure. This will only affect the
-       behavior of nixos-rebuild if <option>--target-host</option> or
-       <option>--build-host</option> is also set. This is useful when
-       the target-host connection to cache.nixos.org is faster than the
-       connection between hosts.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--use-remote-sudo</option>
-    </term>
-    <listitem>
-     <para>
-      When set, nixos-rebuild prefixes remote commands that run on
-      the <option>--build-host</option> and <option>--target-host</option>
-      systems with <command>sudo</command>. Setting this option allows
-      deploying as a non-root user.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--flake</option> <replaceable>flake-uri</replaceable><optional>#<replaceable>name</replaceable></optional>
-    </term>
-    <listitem>
-     <para>
-      Build the NixOS system from the specified flake. It defaults to
-      the directory containing the target of the symlink
-      <filename>/etc/nixos/flake.nix</filename>, if it exists. The
-      flake must contain an output named
-      <literal>nixosConfigurations.<replaceable>name</replaceable></literal>. If
-      <replaceable>name</replaceable> is omitted, it default to the
-      current host name.
-     </para>
-    </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>
-   In addition, <command>nixos-rebuild</command> accepts various Nix-related
-   flags, including <option>--max-jobs</option> / <option>-j</option>, <option>-I</option>,
-   <option>--show-trace</option>, <option>--keep-failed</option>,
-   <option>--keep-going</option>, <option>--impure</option>, and <option>--verbose</option> /
-   <option>-v</option>. See the Nix manual for details.
-  </para>
- </refsection>
-
- <refsection>
-  <title>Environment</title>
-
-  <variablelist>
-   <varlistentry>
-    <term>
-     <envar>NIXOS_CONFIG</envar>
-    </term>
-    <listitem>
-     <para>
-      Path to the main NixOS configuration module. Defaults to
-      <filename>/etc/nixos/configuration.nix</filename>.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <envar>NIX_PATH</envar>
-    </term>
-    <listitem>
-     <para>
-      A colon-separated list of directories used to look up Nix expressions enclosed in angle brackets (e.g &lt;nixpkgs&gt;). Example
-      <screen>
-          nixpkgs=./my-nixpkgs
-      </screen>
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <envar>NIX_SSHOPTS</envar>
-    </term>
-    <listitem>
-     <para>
-      Additional options to be passed to <command>ssh</command> on the command
-      line.
-     </para>
-    </listitem>
-   </varlistentry>
-  </variablelist>
- </refsection>
-
- <refsection>
-  <title>Files</title>
-
-  <variablelist>
-
-   <varlistentry>
-    <term>
-     <filename>/etc/nixos/flake.nix</filename>
-    </term>
-    <listitem>
-     <para>
-      If this file exists, then <command>nixos-rebuild</command> will
-      use it as if the <option>--flake</option> option was given. This
-      file may be a symlink to a <filename>flake.nix</filename> in an
-      actual flake; thus <filename>/etc/nixos</filename> need not be a
-      flake.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <filename>/run/current-system</filename>
-    </term>
-    <listitem>
-     <para>
-      A symlink to the currently active system configuration in the Nix store.
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <filename>/nix/var/nix/profiles/system</filename>
-    </term>
-    <listitem>
-     <para>
-      The Nix profile that contains the current and previous system
-      configurations. Used to generate the GRUB boot menu.
-     </para>
-    </listitem>
-   </varlistentry>
-
-  </variablelist>
- </refsection>
-
- <refsection>
-  <title>Bugs</title>
-  <para>
-   This command should be renamed to something more descriptive.
-  </para>
- </refsection>
-</refentry>
diff --git a/nixos/doc/manual/man-nixos-version.xml b/nixos/doc/manual/man-nixos-version.xml
deleted file mode 100644
index fae25721e39..00000000000
--- a/nixos/doc/manual/man-nixos-version.xml
+++ /dev/null
@@ -1,137 +0,0 @@
-<refentry xmlns="http://docbook.org/ns/docbook"
-          xmlns:xlink="http://www.w3.org/1999/xlink"
-          xmlns:xi="http://www.w3.org/2001/XInclude">
- <refmeta>
-  <refentrytitle><command>nixos-version</command>
-  </refentrytitle><manvolnum>8</manvolnum>
-  <refmiscinfo class="source">NixOS</refmiscinfo>
- </refmeta>
- <refnamediv>
-  <refname><command>nixos-version</command></refname>
-  <refpurpose>show the NixOS version</refpurpose>
- </refnamediv>
- <refsynopsisdiv>
-  <cmdsynopsis>
-   <command>nixos-version</command>
-   <arg>
-    <option>--hash</option>
-   </arg>
-
-   <arg>
-    <option>--revision</option>
-   </arg>
-
-   <arg>
-    <option>--json</option>
-   </arg>
-
-  </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsection>
-  <title>Description</title>
-  <para>
-   This command shows the version of the currently active NixOS configuration.
-   For example:
-<screen><prompt>$ </prompt>nixos-version
-16.03.1011.6317da4 (Emu)
-</screen>
-   The version consists of the following elements:
-   <variablelist>
-    <varlistentry>
-     <term>
-      <literal>16.03</literal>
-     </term>
-     <listitem>
-      <para>
-       The NixOS release, indicating the year and month in which it was
-       released (e.g. March 2016).
-      </para>
-     </listitem>
-    </varlistentry>
-    <varlistentry>
-     <term>
-      <literal>1011</literal>
-     </term>
-     <listitem>
-      <para>
-       The number of commits in the Nixpkgs Git repository between the start of
-       the release branch and the commit from which this version was built.
-       This ensures that NixOS versions are monotonically increasing. It is
-       <literal>git</literal> when the current NixOS configuration was built
-       from a checkout of the Nixpkgs Git repository rather than from a NixOS
-       channel.
-      </para>
-     </listitem>
-    </varlistentry>
-    <varlistentry>
-     <term>
-      <literal>6317da4</literal>
-     </term>
-     <listitem>
-      <para>
-       The first 7 characters of the commit in the Nixpkgs Git repository from
-       which this version was built.
-      </para>
-     </listitem>
-    </varlistentry>
-    <varlistentry>
-     <term>
-      <literal>Emu</literal>
-     </term>
-     <listitem>
-      <para>
-       The code name of the NixOS release. The first letter of the code name
-       indicates that this is the N'th stable NixOS release; for example, Emu
-       is the fifth release.
-      </para>
-     </listitem>
-    </varlistentry>
-   </variablelist>
-  </para>
- </refsection>
-
- <refsection>
-  <title>Options</title>
-
-  <para>
-   This command accepts the following options:
-  </para>
-
-  <variablelist>
-
-   <varlistentry>
-    <term>
-     <option>--hash</option>
-    </term>
-    <term>
-     <option>--revision</option>
-    </term>
-    <listitem>
-     <para>
-      Show the full SHA1 hash of the Git commit from which this configuration
-      was built, e.g.
-<screen><prompt>$ </prompt>nixos-version --hash
-6317da40006f6bc2480c6781999c52d88dde2acf
-</screen>
-     </para>
-    </listitem>
-   </varlistentry>
-
-   <varlistentry>
-    <term>
-     <option>--json</option>
-    </term>
-    <listitem>
-     <para>
-      Print a JSON representation of the versions of NixOS and the
-      top-level configuration flake.
-     </para>
-    </listitem>
-   </varlistentry>
-
-  </variablelist>
-
- </refsection>
-
-</refentry>
diff --git a/nixos/doc/manual/man-pages.xml b/nixos/doc/manual/man-pages.xml
index 58f73521e94..52183f1f9ee 100644
--- a/nixos/doc/manual/man-pages.xml
+++ b/nixos/doc/manual/man-pages.xml
@@ -14,12 +14,33 @@
   <copyright><year>2007-2022</year><holder>Eelco Dolstra and the Nixpkgs/NixOS contributors</holder>
   </copyright>
  </info>
- <xi:include href="man-configuration.xml" />
- <xi:include href="man-nixos-build-vms.xml" />
- <xi:include href="man-nixos-generate-config.xml" />
- <xi:include href="man-nixos-install.xml" />
- <xi:include href="man-nixos-enter.xml" />
- <xi:include href="man-nixos-option.xml" />
- <xi:include href="man-nixos-rebuild.xml" />
- <xi:include href="man-nixos-version.xml" />
+ <refentry>
+  <refmeta>
+   <refentrytitle><filename>configuration.nix</filename>
+   </refentrytitle><manvolnum>5</manvolnum>
+   <refmiscinfo class="source">NixOS</refmiscinfo>
+ <!-- <refmiscinfo class="version"><xi:include href="version.txt" parse="text"/></refmiscinfo> -->
+  </refmeta>
+  <refnamediv>
+   <refname><filename>configuration.nix</filename></refname>
+   <refpurpose>NixOS system configuration specification</refpurpose>
+  </refnamediv>
+  <refsection>
+   <title>Description</title>
+   <para>
+    The file <filename>/etc/nixos/configuration.nix</filename> contains the
+    declarative specification of your NixOS system configuration. The command
+    <command>nixos-rebuild</command> takes this file and realises the system
+    configuration specified therein.
+   </para>
+  </refsection>
+  <refsection>
+   <title>Options</title>
+   <para>
+    You can use the following options in <filename>configuration.nix</filename>.
+   </para>
+   <xi:include href="./generated/options-db.xml"
+             xpointer="configuration-variable-list" />
+  </refsection>
+ </refentry>
 </reference>
diff --git a/nixos/doc/manual/manpages/README.md b/nixos/doc/manual/manpages/README.md
new file mode 100644
index 00000000000..9923f482392
--- /dev/null
+++ b/nixos/doc/manual/manpages/README.md
@@ -0,0 +1,57 @@
+# NixOS manpages
+
+This is the collection of NixOS manpages, excluding `configuration.nix(5)`.
+
+Man pages are written in [`mdoc(7)` format](https://mandoc.bsd.lv/man/mdoc.7.html) and should be portable between mandoc and groff for rendering (though minor differences may occur, mandoc and groff seem to have slightly different spacing rules.)
+
+For previewing edited files, you can just run `man -l path/to/file.8` and you will see it rendered.
+
+Being written in `mdoc` these manpages use semantic markup. This file provides a guideline on where to apply which of the semantic elements of `mdoc`.
+
+### Command lines and arguments
+
+In any manpage, commands, flags and arguments to the *current* executable should be marked according to their semantics. Commands, flags and arguments passed to *other* executables should not be marked like this and should instead be considered as code examples and marked with `Ql`.
+
+ - Use `Fl` to mark flag arguments, `Ar` for their arguments.
+ - Repeating arguments should be marked by adding ellipses (`...`).
+ - Use `Cm` to mark literal string arguments, e.g. the `boot` command argument passed to `nixos-rebuild`.
+ - Optional flags or arguments should be marked with `Op`. This includes optional repeating arguments.
+ - Required flags or arguments should not be marked.
+ - Mutually exclusive groups of arguments should be enclosed in curly brackets, preferrably created with `Bro`/`Brc` blocks.
+
+When an argument is used in an example it should be marked up with `Ar` again to differentiate it from a constant. For example, a command with a `--host name` flag that calls ssh to retrieve the host's local time would signify this thusly:
+```
+This will run
+.Ic ssh Ar name Ic time
+to retrieve the remote time.
+```
+
+### Paths, NixOS options, environment variables
+
+Constant paths should be marked with `Pa`, NixOS options with `Va`, and environment variables with `Ev`.
+
+Generated paths, e.g. `result/bin/run-hostname-vm` (where `hostname` is a variable or arguments) should be marked as `Ql` inline literals with their variable components marked appropriately.
+
+ - Taking `hostname` from an argument become `.Ql result/bin/run- Ns Ar hostname Ns -vm`
+ - Taking `hostname` from a variable otherwise defined becomes `.Ql result/bin/run- Ns Va hostname Ns -vm`
+
+### Code examples and other commands
+
+In free text names and complete invocations of other commands (e.g. `ssh` or `tar -xvf src.tar`) should be marked with `Ic`, fragments of command lines should be marked with `Ql`.
+
+Larger code blocks or those that cannot be shown inline should use indented literal display block markup for their contents, i.e.
+```
+.Bd -literal -offset indent
+...
+.Ed
+```
+Contents of code blocks may be marked up further, e.g. if they refer to arguments that will be subsituted into them:
+```
+.Bd -literal -offset indent
+{
+  options.hostname = "\c
+.Ar hostname Ns \c
+";
+}
+.Ed
+```
diff --git a/nixos/doc/manual/manpages/nixos-build-vms.8 b/nixos/doc/manual/manpages/nixos-build-vms.8
new file mode 100644
index 00000000000..6a8f2c42edd
--- /dev/null
+++ b/nixos/doc/manual/manpages/nixos-build-vms.8
@@ -0,0 +1,105 @@
+.Dd January 1, 1980
+.Dt nixos-build-vms 8
+.Os
+.Sh NAME
+.Nm nixos-build-vms
+.Nd build a network of virtual machines from a network of NixOS configurations
+.
+.
+.
+.Sh SYNOPSIS
+.Nm nixos-build-vms
+.Op Fl -show-trace
+.Op Fl -no-out-link
+.Op Fl -help
+.Op Fl -option Ar name value
+.Pa network.nix
+.
+.
+.
+.Sh DESCRIPTION
+.
+This command builds a network of QEMU\-KVM virtual machines of a Nix expression
+specifying a network of NixOS machines. The virtual network can be started by
+executing the
+.Pa bin/run-vms
+shell script that is generated by this command. By default, a
+.Pa result
+symlink is produced that points to the generated virtual network.
+.
+.Pp
+A network Nix expression has the following structure:
+.Bd -literal -offset indent
+{
+  test1 = {pkgs, config, ...}:
+    {
+      services.openssh.enable = true;
+      nixpkgs.localSystem.system = "i686-linux";
+      deployment.targetHost = "test1.example.net";
+
+      # Other NixOS options
+    };
+
+  test2 = {pkgs, config, ...}:
+    {
+      services.openssh.enable = true;
+      services.httpd.enable = true;
+      environment.systemPackages = [ pkgs.lynx ];
+      nixpkgs.localSystem.system = "x86_64-linux";
+      deployment.targetHost = "test2.example.net";
+
+      # Other NixOS options
+    };
+}
+.Ed
+.
+.Pp
+Each attribute in the expression represents a machine in the network
+.Ns (e.g.
+.Va test1
+and
+.Va test2 Ns
+) referring to a function defining a NixOS configuration. In each NixOS
+configuration, two attributes have a special meaning. The
+.Va deployment.targetHost
+specifies the address (domain name or IP address) of the system which is used by
+.Ic ssh
+to perform remote deployment operations. The
+.Va nixpkgs.localSystem.system
+attribute can be used to specify an architecture for the target machine, such as
+.Ql i686-linux
+which builds a 32-bit NixOS configuration. Omitting this property will build the
+configuration for the same architecture as the host system.
+.
+.
+.
+.Sh OPTIONS
+.Bl -tag -width indent
+.It Fl -show-trace
+Shows a trace of the output.
+.
+.It Fl -no-out-link
+Do not create a
+.Pa result
+symlink.
+.
+.It Fl h , -help
+Shows the usage of this command to the user.
+.
+.It Fl -option Ar name Va value
+Set the Nix configuration option
+.Va name
+to
+.Va value Ns
+\&. This overrides settings in the Nix configuration file (see
+.Xr nix.conf 5 Ns
+).
+.El
+.
+.
+.
+.Sh AUTHORS
+.An -nosplit
+.An Eelco Dolstra
+and
+.An the Nixpkgs/NixOS contributors
diff --git a/nixos/doc/manual/manpages/nixos-enter.8 b/nixos/doc/manual/manpages/nixos-enter.8
new file mode 100644
index 00000000000..646f92199d6
--- /dev/null
+++ b/nixos/doc/manual/manpages/nixos-enter.8
@@ -0,0 +1,72 @@
+.Dd January 1, 1980
+.Dt nixos-enter 8
+.Os
+.Sh NAME
+.Nm nixos-enter
+.Nd run a command in a NixOS chroot environment
+.
+.
+.
+.Sh SYNOPSIS
+.Nm nixos-enter
+.Op Fl -root Ar root
+.Op Fl -system Ar system
+.Op Fl -command | c Ar shell-command
+.Op Fl -silent
+.Op Fl -help
+.Op Fl - Ar arguments ...
+.
+.
+.
+.Sh DESCRIPTION
+This command runs a command in a NixOS chroot environment, that is, in a filesystem hierarchy previously prepared using
+.Xr nixos-install 8 .
+.
+.
+.
+.Sh OPTIONS
+.Bl -tag -width indent
+.It Fl -root Ar root
+The path to the NixOS system you want to enter. It defaults to
+.Pa /mnt Ns
+\&.
+.It Fl -system Ar system
+The NixOS system configuration to use. It defaults to
+.Pa /nix/var/nix/profiles/system Ns
+\&. You can enter a previous NixOS configuration by specifying a path such as
+.Pa /nix/var/nix/profiles/system-106-link Ns
+\&.
+.
+.It Fl -command Ar shell-command , Fl c Ar shell-command
+The bash command to execute.
+.
+.It Fl -silent
+Suppresses all output from the activation script of the target system.
+.
+.It Fl -
+Interpret the remaining arguments as the program name and arguments to be invoked.
+The program is not executed in a shell.
+.El
+.
+.
+.
+.Sh EXAMPLES
+.Bl -tag -width indent
+.It Ic nixos-enter --root /mnt
+Start an interactive shell in the NixOS installation in
+.Pa /mnt Ns .
+.
+.It Ic nixos-enter -c 'ls -l /; cat /proc/mounts'
+Run a shell command.
+.
+.It Ic nixos-enter -- cat /proc/mounts
+Run a non-shell command.
+.El
+.
+.
+.
+.Sh AUTHORS
+.An -nosplit
+.An Eelco Dolstra
+and
+.An the Nixpkgs/NixOS contributors
diff --git a/nixos/doc/manual/manpages/nixos-generate-config.8 b/nixos/doc/manual/manpages/nixos-generate-config.8
new file mode 100644
index 00000000000..1b95599e156
--- /dev/null
+++ b/nixos/doc/manual/manpages/nixos-generate-config.8
@@ -0,0 +1,165 @@
+.Dd January 1, 1980
+.Dt nixos-generate-config 8
+.Os
+.Sh NAME
+.Nm nixos-generate-config
+.Nd generate NixOS configuration modules
+.
+.
+.
+.Sh SYNOPSIS
+.Nm nixos-generate-config
+.Op Fl -force
+.Op Fl -root Ar root
+.Op Fl -dir Ar dir
+.
+.
+.
+.Sh DESCRIPTION
+This command writes two NixOS configuration modules:
+.Bl -tag -width indent
+.It Pa /etc/nixos/hardware-configuration.nix
+This module sets NixOS configuration options based on your current hardware
+configuration. In particular, it sets the
+.Va fileSystem
+option to reflect all currently mounted file systems, the
+.Va swapDevices
+option to reflect active swap devices, and the
+.Va boot.initrd.*
+options to ensure that the initial ramdisk contains any kernel modules necessary
+for mounting the root file system.
+.Pp
+If this file already exists, it is overwritten. Thus, you should not modify it
+manually. Rather, you should include it from your
+.Pa /etc/nixos/configuration.nix Ns
+, and re-run
+.Nm
+to update it whenever your hardware configuration changes.
+.
+.It Pa /etc/nixos/configuration.nix
+This is the main NixOS system configuration module. If it already exists, it’s
+left unchanged. Otherwise,
+.Nm
+will write a template for you to customise.
+.El
+.
+.
+.
+.Sh OPTIONS
+.Bl -tag -width indent
+.It Fl -root Ar root
+If this option is given, treat the directory
+.Ar root
+as the root of the file system. This means that configuration files will be written to
+.Ql Ar root Ns /etc/nixos Ns
+, and that any file systems outside of
+.Ar root
+are ignored for the purpose of generating the
+.Va fileSystems
+option.
+.
+.It Fl -dir Ar dir
+If this option is given, write the configuration files to the directory
+.Ar dir
+instead of
+.Pa /etc/nixos Ns
+\&.
+.
+.It Fl -force
+Overwrite
+.Pa /etc/nixos/configuration.nix
+if it already exists.
+.
+.It Fl -no-filesystems
+Omit everything concerning file systems and swap devices from the hardware configuration.
+.
+.It Fl -show-hardware-config
+Don't generate
+.Pa configuration.nix
+or
+.Pa hardware-configuration.nix
+and print the hardware configuration to stdout only.
+.El
+.
+.
+.
+.Sh EXAMPLES
+This command is typically used during NixOS installation to write initial
+configuration modules. For example, if you created and mounted the target file
+systems on
+.Pa /mnt
+and
+.Pa /mnt/boot Ns
+, you would run:
+.Bd -literal -offset indent
+$ nixos-generate-config --root /mnt
+.Ed
+.
+.Pp
+The resulting file
+.Pa /mnt/etc/nixos/hardware-configuration.nix
+might look like this:
+.Bd -literal -offset indent
+# Do not modify this file!  It was generated by 'nixos-generate-config'
+# and may be overwritten by future invocations.  Please make changes
+# to /etc/nixos/configuration.nix instead.
+{ config, pkgs, ... }:
+
+{
+  imports =
+    [ <nixos/modules/installer/scan/not-detected.nix>
+    ];
+
+  boot.initrd.availableKernelModules = [ "ehci_hcd" "ahci" ];
+  boot.kernelModules = [ "kvm-intel" ];
+  boot.extraModulePackages = [ ];
+
+  fileSystems."/" =
+    { device = "/dev/disk/by-label/nixos";
+      fsType = "ext3";
+      options = [ "rw" "data=ordered" "relatime" ];
+    };
+
+  fileSystems."/boot" =
+    { device = "/dev/sda1";
+      fsType = "ext3";
+      options = [ "rw" "errors=continue" "user_xattr" "acl" "barrier=1" "data=writeback" "relatime" ];
+    };
+
+  swapDevices =
+    [ { device = "/dev/sda2"; }
+    ];
+
+  nix.maxJobs = 8;
+}
+.Ed
+.
+.Pp
+It will also create a basic
+.Pa /mnt/etc/nixos/configuration.nix Ns
+, which you should edit to customise the logical configuration of your system. \
+This file includes the result of the hardware scan as follows:
+.Bd -literal -offset indent
+imports = [ ./hardware-configuration.nix ];
+.Ed
+.
+.Pp
+After installation, if your hardware configuration changes, you can run:
+.Bd -literal -offset indent
+$ nixos-generate-config
+.Ed
+.
+.Pp
+to update
+.Pa /etc/nixos/hardware-configuration.nix Ns
+\&. Your
+.Pa /etc/nixos/configuration.nix
+will
+.Em not
+be overwritten.
+.
+.Sh AUTHORS
+.An -nosplit
+.An Eelco Dolstra
+and
+.An the Nixpkgs/NixOS contributors
diff --git a/nixos/doc/manual/manpages/nixos-install.8 b/nixos/doc/manual/manpages/nixos-install.8
new file mode 100644
index 00000000000..c6c8ed15224
--- /dev/null
+++ b/nixos/doc/manual/manpages/nixos-install.8
@@ -0,0 +1,191 @@
+.Dd January 1, 1980
+.Dt nixos-install 8
+.Os
+.Sh NAME
+.Nm nixos-install
+.Nd install bootloader and NixOS
+.
+.
+.
+.Sh SYNOPSIS
+.Nm nixos-install
+.Op Fl -verbose | v
+.Op Fl I Ar path
+.Op Fl -root Ar root
+.Op Fl -system Ar path
+.Op Fl -flake Ar flake-uri
+.Op Fl -impure
+.Op Fl -channel Ar channel
+.Op Fl -no-channel-copy
+.Op Fl -no-root-password | -no-root-passwd
+.Op Fl -no-bootloader
+.Op Fl -max-jobs | j Ar number
+.Op Fl -cores Ar number
+.Op Fl -option Ar name value
+.Op Fl -show-trace
+.Op Fl -keep-going
+.Op Fl -help
+.
+.
+.
+.Sh DESCRIPTION
+This command installs NixOS in the file system mounted on
+.Pa /mnt Ns
+, based on the NixOS configuration specified in
+.Pa /mnt/etc/nixos/configuration.nix Ns
+\&. It performs the following steps:
+.
+.Bl -enum
+.It
+It copies Nix and its dependencies to
+.Pa /mnt/nix/store Ns
+\&.
+.
+.It
+It runs Nix in
+.Pa /mnt
+to build the NixOS configuration specified in
+.Pa /mnt/etc/nixos/configuration.nix Ns
+\&.
+.
+.It
+It installs the current channel
+.Dq nixos
+in the target channel profile (unless
+.Fl -no-channel-copy
+is specified).
+.
+.It
+It installs the GRUB boot loader on the device specified in the option
+.Va boot.loader.grub.device
+(unless
+.Fl -no-bootloader
+is specified), and generates a GRUB configuration file that boots into the NixOS
+configuration just installed.
+.
+.It
+It prompts you for a password for the root account (unless
+.Fl -no-root-password
+is specified).
+.El
+.
+.Pp
+This command is idempotent: if it is interrupted or fails due to a temporary
+problem (e.g. a network issue), you can safely re-run it.
+.
+.
+.
+.Sh OPTIONS
+.Bl -tag -width indent
+.It Fl -verbose , v
+Increases the level of verbosity of diagnostic messages printed on standard
+error. For each Nix operation, the information printed on standard output is
+well-defined; any diagnostic information is printed on standard error, never on
+standard output.
+.Pp
+Please note that this option may be specified repeatedly.
+.
+.It Fl -root Ar root
+Defaults to
+.Pa /mnt Ns
+\&. If this option is given, treat the directory
+.Ar root
+as the root of the NixOS installation.
+.
+.It Fl -system Ar path
+If this option is provided,
+.Nm
+will install the specified closure rather than attempt to build one from
+.Pa /mnt/etc/nixos/configuration.nix Ns
+\&.
+.Pp
+The closure must be an appropriately configured NixOS system, with boot loader
+and partition configuration that fits the target host. Such a closure is
+typically obtained with a command such as
+.Ic nix-build -I nixos-config=./configuration.nix '<nixpkgs/nixos>' -A system --no-out-link Ns
+\&.
+.
+.It Fl -flake Ar flake-uri Ns # Ns Ar name
+Build the NixOS system from the specified flake. The flake must contain an
+output named
+.Ql nixosConfigurations. Ns Ar name Ns
+\&.
+.
+.It Fl -channel Ar channel
+If this option is provided, do not copy the current
+.Dq nixos
+channel to the target host. Instead, use the specified derivation.
+.
+.It Fl I Ar Path
+Add a path to the Nix expression search path. This option may be given multiple
+times. See the
+.Ev NIX_PATH
+environment variable for information on the semantics of the Nix search path. Paths added through
+.Fl I
+take precedence over
+.Ev NIX_PATH Ns
+\&.
+.
+.It Fl -max-jobs , j Ar number
+Sets the maximum number of build jobs that Nix will perform in parallel to the
+specified number. The default is 1. A higher value is useful on SMP systems or
+to exploit I/O latency.
+.
+.It Fl -cores Ar N
+Sets the value of the
+.Ev NIX_BUILD_CORES
+environment variable in the invocation of builders. Builders can use this
+variable at their discretion to control the maximum amount of parallelism. For
+instance, in Nixpkgs, if the derivation attribute
+.Va enableParallelBuilding
+is set to true, the builder passes the
+.Fl j Ns Va N
+flag to GNU Make. The value 0 means that the builder should use all available CPU cores in the system.
+.
+.It Fl -option Ar name value
+Set the Nix configuration option
+.Ar name
+to
+.Ar value Ns
+\&.
+.
+.It Fl -show-trace
+Causes Nix to print out a stack trace in case of Nix expression evaluation errors.
+.
+.It Fl -keep-going
+Causes Nix to continue building derivations as far as possible in the face of failed builds.
+.
+.It Fl -help
+Synonym for
+.Ic man nixos-install Ns
+\&.
+.El
+.
+.
+.
+.Sh EXAMPLES
+A typical NixOS installation is done by creating and mounting a file system on
+.Pa /mnt Ns
+, generating a NixOS configuration in
+.Pa /mnt/etc/nixos/configuration.nix Ns
+, and running
+.Nm Ns
+\&. For instance, if we want to install NixOS on an ext4 file system created in
+.Pa /dev/sda1 Ns
+:
+.Bd -literal -offset indent
+$ mkfs.ext4 /dev/sda1
+$ mount /dev/sda1 /mnt
+$ nixos-generate-config --root /mnt
+$ # edit /mnt/etc/nixos/configuration.nix
+$ nixos-install
+$ reboot
+.Ed
+.
+.
+.
+.Sh AUTHORS
+.An -nosplit
+.An Eelco Dolstra
+and
+.An the Nixpkgs/NixOS contributors
diff --git a/nixos/doc/manual/manpages/nixos-option.8 b/nixos/doc/manual/manpages/nixos-option.8
new file mode 100644
index 00000000000..28438b03580
--- /dev/null
+++ b/nixos/doc/manual/manpages/nixos-option.8
@@ -0,0 +1,89 @@
+.Dd January 1, 1980
+.Dt nixos-option 8
+.Os
+.Sh NAME
+.Nm nixos-option
+.Nd inspect a NixOS configuration
+.
+.
+.
+.Sh SYNOPSIS
+.Nm
+.Op Fl r | -recursive
+.Op Fl I Ar path
+.Ar option.name
+.
+.
+.
+.Sh DESCRIPTION
+This command evaluates the configuration specified in
+.Pa /etc/nixos/configuration.nix
+and returns the properties of the option name given as argument.
+.
+.Pp
+When the option name is not an option, the command prints the list of attributes
+contained in the attribute set.
+.
+.
+.
+.Sh OPTIONS
+.Bl -tag -width indent
+.It Fl r , -recursive
+Print all the values at or below the specified path recursively.
+.
+.It Fl I Ar path
+This option is passed to the underlying
+.Xr nix-instantiate 1
+invocation.
+.El
+.
+.
+.
+.Sh ENVIRONMENT
+.Bl -tag -width indent
+.It Ev NIXOS_CONFIG
+Path to the main NixOS configuration module. Defaults to
+.Pa /etc/nixos/configuration.nix Ns
+\&.
+.El
+.
+.
+.
+.Sh EXAMPLES
+Investigate option values:
+.Bd -literal -offset indent
+$ nixos-option boot.loader
+This attribute set contains:
+generationsDir
+grub
+initScript
+
+$ nixos-option boot.loader.grub.enable
+Value:
+true
+
+Default:
+true
+
+Description:
+Whether to enable the GNU GRUB boot loader.
+
+Declared by:
+  "/nix/var/nix/profiles/per-user/root/channels/nixos/nixpkgs/nixos/modules/system/boot/loader/grub/grub.nix"
+
+Defined by:
+  "/nix/var/nix/profiles/per-user/root/channels/nixos/nixpkgs/nixos/modules/system/boot/loader/grub/grub.nix"
+.Ed
+.
+.
+.
+.Sh SEE ALSO
+.Xr configuration.nix 5
+.
+.
+.
+.Sh AUTHORS
+.An -nosplit
+.An Nicolas Pierron
+and
+.An the Nixpkgs/NixOS contributors
diff --git a/nixos/doc/manual/manpages/nixos-rebuild.8 b/nixos/doc/manual/manpages/nixos-rebuild.8
new file mode 100644
index 00000000000..64bbbee411d
--- /dev/null
+++ b/nixos/doc/manual/manpages/nixos-rebuild.8
@@ -0,0 +1,452 @@
+.Dd January 1, 1980
+.Dt nixos-rebuild 8
+.Os
+.Sh NAME
+.Nm nixos-rebuild
+.Nd reconfigure a NixOS machine
+.
+.
+.
+.Sh SYNOPSIS
+.Nm
+.Bro
+.Cm switch | boot | test | build | dry-build | dry-activate | edit | build-vm | build-vm-with-bootloader
+.Brc
+.br
+.Op Fl -upgrade | -upgrade-all
+.Op Fl -install-bootloader
+.Op Fl -no-build-nix
+.Op Fl -fast
+.Op Fl -rollback
+.Op Fl -builders Ar builder-spec
+.br
+.Op Fl -flake Ar flake-uri
+.Op Fl -no-flake
+.Op Fl -override-input Ar input-name flake-uri
+.br
+.Op Fl -profile-name | p Ar name
+.Op Fl -specialisation | c Ar name
+.br
+.Op Fl -build-host Va host
+.Op Fl -target-host Va host
+.Op Fl -use-remote-sudo
+.br
+.Op Fl -show-trace
+.Op Fl I Va NIX_PATH
+.Op Fl -verbose | v
+.Op Fl -impure
+.Op Fl -max-jobs | j Va number
+.Op Fl -keep-failed | K
+.Op Fl -keep-going | k
+.
+.
+.
+.Sh DESCRIPTION
+This command updates the system so that it corresponds to the
+configuration specified in
+.Pa /etc/nixos/configuration.nix
+or
+.Pa /etc/nixos/flake.nix Ns
+\&. Thus, every time you modify the configuration or any other NixOS module, you
+must run
+.Nm
+to make the changes take effect. It builds the new system in
+.Pa /nix/store Ns
+, runs its activation script, and stop and (re)starts any system services if
+needed. Please note that user services need to be started manually as they
+aren't detected by the activation script at the moment.
+.
+.Pp
+This command has one required argument, which specifies the desired
+operation. It must be one of the following:
+.Bl -tag -width indent
+.It Cm switch
+Build and activate the new configuration, and make it the boot default. That
+is, the configuration is added to the GRUB boot menu as the default
+menu entry, so that subsequent reboots will boot the system into the new
+configuration. Previous configurations activated with
+.Ic nixos-rebuild switch
+or
+.Ic nixos-rebuild boot
+remain available in the GRUB menu.
+.Pp
+Note that if you are using specializations, running just
+.Ic nixos-rebuild switch
+will switch you back to the unspecialized, base system \(em in that case, you
+might want to use this instead:
+.Bd -literal -offset indent
+$ nixos-rebuild switch --specialisation your-specialisation-name
+.Ed
+.Pp
+This command will build all specialisations and make them bootable just
+like regular
+.Ic nixos-rebuild switch
+does \(em the only thing different is that it will switch to given
+specialisation instead of the base system; it can be also used to switch from
+the base system into a specialised one, or to switch between specialisations.
+.
+.It Cm boot
+Build the new configuration and make it the boot default (as with
+.Ic nixos-rebuild switch Ns
+), but do not activate it. That is, the system continues to run the previous
+configuration until the next reboot.
+.
+.It Cm test
+Build and activate the new configuration, but do not add it to the GRUB
+boot menu. Thus, if you reboot the system (or if it crashes), you will
+automatically revert to the default configuration (i.e. the
+configuration resulting from the last call to
+.Ic nixos-rebuild switch
+or
+.Ic nixos-rebuild boot Ns
+).
+.Pp
+Note that if you are using specialisations, running just
+.Ic nixos-rebuild test
+will activate the unspecialised, base system \(em in that case, you might want
+to use this instead:
+.Bd -literal -offset indent
+$ nixos-rebuild test --specialisation your-specialisation-name
+.Ed
+.Pp
+This command can be also used to switch from the base system into a
+specialised one, or to switch between specialisations.
+.
+.It Cm build
+Build the new configuration, but neither activate it nor add it to the
+GRUB boot menu. It leaves a symlink named
+.Pa result
+in the current directory, which points to the output of the top-level
+.Dq system
+derivation. This is essentially the same as doing
+.Bd -literal -offset indent
+$ nix-build /path/to/nixpkgs/nixos -A system
+.Ed
+.Pp
+Note that you do not need to be root to run
+.Ic nixos-rebuild build Ns
+\&.
+.
+.It Cm dry-build
+Show what store paths would be built or downloaded by any of the
+operations above, but otherwise do nothing.
+.
+.It Cm dry-activate
+Build the new configuration, but instead of activating it, show what
+changes would be performed by the activation (i.e. by
+.Ic nixos-rebuild test Ns
+). For instance, this command will print which systemd units would be restarted.
+The list of changes is not guaranteed to be complete.
+.
+.It Cm edit
+Opens
+.Pa configuration.nix
+in the default editor.
+.
+.It Cm build-vm
+Build a script that starts a NixOS virtual machine with the desired
+configuration. It leaves a symlink
+.Pa result
+in the current directory that points (under
+.Ql result/bin/run\- Ns Va hostname Ns \-vm Ns
+)
+at the script that starts the VM. Thus, to test a NixOS configuration in
+a virtual machine, you should do the following:
+.Bd -literal -offset indent
+$ nixos-rebuild build-vm
+$ ./result/bin/run-*-vm
+.Ed
+.Pp
+The VM is implemented using the
+.Ql qemu
+package. For best performance, you should load the
+.Ql kvm-intel
+or
+.Ql kvm-amd
+kernel modules to get hardware virtualisation.
+.Pp
+The VM mounts the Nix store of the host through the 9P file system. The
+host Nix store is read-only, so Nix commands that modify the Nix store
+will not work in the VM. This includes commands such as
+.Nm Ns
+; to change the VM’s configuration, you must halt the VM and re-run the commands
+above.
+.Pp
+The VM has its own ext3 root file system, which is automatically created when
+the VM is first started, and is persistent across reboots of the VM. It is
+stored in
+.Ql ./ Ns Va hostname Ns .qcow2 Ns
+\&.
+.\" The entire file system hierarchy of the host is available in
+.\" the VM under
+.\" .Pa /hostfs Ns
+.\" .
+.
+.It Cm build-vm-with-bootloader
+Like
+.Cm build-vm Ns
+, but boots using the regular boot loader of your configuration (e.g. GRUB 1 or
+2), rather than booting directly into the kernel and initial ramdisk of the
+system. This allows you to test whether the boot loader works correctly. \
+However, it does not guarantee that your NixOS configuration will boot
+successfully on the host hardware (i.e., after running
+.Ic nixos-rebuild switch Ns
+), because the hardware and boot loader configuration in the VM are different.
+The boot loader is installed on an automatically generated virtual disk
+containing a
+.Pa /boot
+partition.
+.El
+.
+.
+.
+.Sh OPTIONS
+.Bl -tag -width indent
+.It Fl -upgrade , -upgrade-all
+Update the root user's channel named
+.Ql nixos
+before rebuilding the system.
+.Pp
+In addition to the
+.Ql nixos
+channel, the root user's channels which have a file named
+.Ql .update-on-nixos-rebuild
+in their base directory will also be updated.
+.Pp
+Passing
+.Fl -upgrade-all
+updates all of the root user's channels.
+.
+.It Fl -install-bootloader
+Causes the boot loader to be (re)installed on the device specified by the
+relevant configuration options.
+.
+.It Fl -no-build-nix
+Normally,
+.Nm
+first builds the
+.Ql nixUnstable
+attribute in Nixpkgs, and uses the resulting instance of the Nix package manager
+to build the new system configuration. This is necessary if the NixOS modules
+use features not provided by the currently installed version of Nix. This option
+disables building a new Nix.
+.
+.It Fl -fast
+Equivalent to
+.Fl -no-build-nix Ns
+\&. This option is useful if you call
+.Nm
+frequently (e.g. if you’re hacking on a NixOS module).
+.
+.It Fl -rollback
+Instead of building a new configuration as specified by
+.Pa /etc/nixos/configuration.nix Ns
+, roll back to the previous configuration. (The previous configuration is
+defined as the one before the “current” generation of the Nix profile
+.Pa /nix/var/nix/profiles/system Ns
+\&.)
+.
+.It Fl -builders Ar builder-spec
+Allow ad-hoc remote builders for building the new system. This requires
+the user executing
+.Nm
+(usually root) to be configured as a trusted user in the Nix daemon. This can be
+achieved by using the
+.Va nix.settings.trusted-users
+NixOS option. Examples values for that option are described in the
+.Dq Remote builds
+chapter in the Nix manual, (i.e.
+.Ql --builders \(dqssh://bigbrother x86_64-linux\(dq Ns
+). By specifying an empty string existing builders specified in
+.Pa /etc/nix/machines
+can be ignored:
+.Ql --builders \(dq\(dq
+for example when they are not reachable due to network connectivity.
+.
+.It Fl -profile-name Ar name , Fl p Ar name
+Instead of using the Nix profile
+.Pa /nix/var/nix/profiles/system
+to keep track of the current and previous system configurations, use
+.Pa /nix/var/nix/profiles/system-profiles/ Ns Va name Ns
+\&. When you use GRUB 2, for every system profile created with this flag, NixOS
+will create a submenu named
+.Dq NixOS - Profile Va name
+in GRUB’s boot menu, containing the current and previous configurations of this profile.
+.Pp
+For instance, if you want to test a configuration file named
+.Pa test.nix
+without affecting the default system profile, you would do:
+.Bd -literal -offset indent
+$ nixos-rebuild switch -p test -I nixos-config=./test.nix
+.Ed
+.Pp
+The new configuration will appear in the GRUB 2 submenu
+.Dq NixOS - Profile 'test' Ns
+\&.
+.
+.It Fl -specialisation Ar name , Fl c Ar name
+Activates given specialisation; when not specified, switching and testing
+will activate the base, unspecialised system.
+.
+.It Fl -build-host Ar host
+Instead of building the new configuration locally, use the specified host
+to perform the build. The host needs to be accessible with
+.Ic ssh Ns ,
+and must be able to perform Nix builds. If the option
+.Fl -target-host
+is not set, the build will be copied back to the local machine when done.
+.Pp
+Note that, if
+.Fl -no-build-nix
+is not specified, Nix will be built both locally and remotely. This is because
+the configuration will always be evaluated locally even though the building
+might be performed remotely.
+.Pp
+You can include a remote user name in the host name
+.Ns ( Va user@host Ns
+). You can also set ssh options by defining the
+.Ev NIX_SSHOPTS
+environment variable.
+.
+.It Fl -target-host Ar host
+Specifies the NixOS target host. By setting this to something other than an
+empty string, the system activation will happen on the remote host instead of
+the local machine. The remote host needs to be accessible over
+.Ic ssh Ns ,
+and for the commands
+.Cm switch Ns
+,
+.Cm boot
+and
+.Cm test
+you need root access.
+.Pp
+If
+.Fl -build-host
+is not explicitly specified or empty, building will take place locally.
+.Pp
+You can include a remote user name in the host name
+.Ns ( Va user@host Ns
+). You can also set ssh options by defining the
+.Ev NIX_SSHOPTS
+environment variable.
+.Pp
+Note that
+.Nm
+honors the
+.Va nixpkgs.crossSystem
+setting of the given configuration but disregards the true architecture of the
+target host. Hence the
+.Va nixpkgs.crossSystem
+setting has to match the target platform or else activation will fail.
+.
+.It Fl -use-substitutes
+When set, nixos-rebuild will add
+.Fl -use-substitutes
+to each invocation of nix-copy-closure. This will only affect the behavior of
+nixos-rebuild if
+.Fl -target-host
+or
+.Fl -build-host
+is also set. This is useful when the target-host connection to cache.nixos.org
+is faster than the connection between hosts.
+.
+.It Fl -use-remote-sudo
+When set, nixos-rebuild prefixes remote commands that run on the
+.Fl -build-host
+and
+.Fl -target-host
+systems with
+.Ic sudo Ns
+\&. Setting this option allows deploying as a non-root user.
+.
+.It Fl -flake Va flake-uri Ns Op Va #name
+Build the NixOS system from the specified flake. It defaults to the directory
+containing the target of the symlink
+.Pa /etc/nixos/flake.nix Ns
+, if it exists. The flake must contain an output named
+.Ql nixosConfigurations. Ns Va name Ns
+\&. If
+.Va name
+is omitted, it default to the current host name.
+.
+.It Fl -no-flake
+Do not imply
+.Fl -flake
+if
+.Pa /etc/nixos/flake.nix
+exists. With this option, it is possible to build non-flake NixOS configurations
+even if the current NixOS systems uses flakes.
+.El
+.Pp
+In addition,
+.Nm
+accepts various Nix-related flags, including
+.Fl -max-jobs Ns ,
+.Fl j Ns ,
+.Fl I Ns ,
+.Fl -show-trace Ns ,
+.Fl -keep-failed Ns ,
+.Fl -keep-going Ns ,
+.Fl -impure Ns ,
+.Fl -verbose Ns , and
+.Fl v Ns
+\&. See the Nix manual for details.
+.
+.
+.
+.Sh ENVIRONMENT
+.Bl -tag -width indent
+.It Ev NIXOS_CONFIG
+Path to the main NixOS configuration module. Defaults to
+.Pa /etc/nixos/configuration.nix Ns
+\&.
+.
+.It Ev NIX_PATH
+A colon-separated list of directories used to look up Nix expressions enclosed
+in angle brackets (e.g. <nixpkgs>). Example:
+.Bd -literal -offset indent
+nixpkgs=./my-nixpkgs
+.Ed
+.
+.It Ev NIX_SSHOPTS
+Additional options to be passed to
+.Ic ssh
+on the command line.
+.El
+.
+.
+.
+.Sh FILES
+.Bl -tag -width indent
+.It Pa /etc/nixos/flake.nix
+If this file exists, then
+.Nm
+will use it as if the
+.Fl -flake
+option was given. This file may be a symlink to a
+.Pa flake.nix
+in an actual flake; thus
+.Pa /etc/nixos
+need not be a flake.
+.
+.It Pa /run/current-system
+A symlink to the currently active system configuration in the Nix store.
+.
+.It Pa /nix/var/nix/profiles/system
+The Nix profile that contains the current and previous system
+configurations. Used to generate the GRUB boot menu.
+.El
+.
+.
+.
+.Sh BUGS
+This command should be renamed to something more descriptive.
+.
+.
+.
+.Sh AUTHORS
+.An -nosplit
+.An Eelco Dolstra
+and
+.An the Nixpkgs/NixOS contributors
diff --git a/nixos/doc/manual/manpages/nixos-version.8 b/nixos/doc/manual/manpages/nixos-version.8
new file mode 100644
index 00000000000..f661611599f
--- /dev/null
+++ b/nixos/doc/manual/manpages/nixos-version.8
@@ -0,0 +1,86 @@
+.Dd January 1, 1980
+.Dt nixos-version 8
+.Os
+.Sh NAME
+.Nm nixos-version
+.Nd show the NixOS version
+.
+.
+.
+.Sh SYNOPSIS
+.Nm nixos-version
+.Op Fl -hash
+.Op Fl -revision
+.Op Fl -configuration-revision
+.Op Fl -json
+.
+.
+.
+.Sh DESCRIPTION
+This command shows the version of the currently active NixOS configuration. For example:
+.Bd -literal -offset indent
+$ nixos-version
+16.03.1011.6317da4 (Emu)
+.Ed
+.
+.Pp
+The version consists of the following elements:
+.Bl -tag -width indent
+.It Ql 16.03
+The NixOS release, indicating the year and month in which it was released
+(e.g. March 2016).
+.It Ql 1011
+The number of commits in the Nixpkgs Git repository between the start of the
+release branch and the commit from which this version was built. This ensures
+that NixOS versions are monotonically increasing. It is
+.Ql git
+when the current NixOS configuration was built from a checkout of the Nixpkgs
+Git repository rather than from a NixOS channel.
+.It Ql 6317da4
+The first 7 characters of the commit in the Nixpkgs Git repository from which
+this version was built.
+.It Ql Emu
+The code name of the NixOS release. The first letter of the code name indicates
+that this is the N'th stable NixOS release; for example, Emu is the fifth
+release.
+.El
+.
+.
+.
+.Sh OPTIONS
+.Bl -tag -width indent
+.It Fl -hash , -revision
+Show the full SHA1 hash of the Git commit from which this configuration was
+built, e.g.
+.Bd -literal -offset indent
+$ nixos-version --hash
+6317da40006f6bc2480c6781999c52d88dde2acf
+.Ed
+.
+.It Fl -configuration-revision
+Show the configuration revision if available. This could be the full SHA1 hash
+of the Git commit of the system flake, if you add
+.Bd -literal -offset indent
+{ system.configurationRevision = self.rev or "dirty"; }
+.Ed
+.Pp
+to the
+.Ql modules
+array of your flake.nix system configuration e.g.
+.Bd -literal -offset indent
+$ nixos-version --configuration-revision
+aa314ebd1592f6cdd53cb5bba8bcae97d9323de8
+.Ed
+.
+.It Fl -json
+Print a JSON representation of the versions of NixOS and the top-level
+configuration flake.
+.El
+.
+.
+.
+.Sh AUTHORS
+.An -nosplit
+.An Eelco Dolstra
+and
+.An the Nixpkgs/NixOS contributors
diff --git a/nixos/doc/manual/manual.md b/nixos/doc/manual/manual.md
new file mode 100644
index 00000000000..8cb766eeccf
--- /dev/null
+++ b/nixos/doc/manual/manual.md
@@ -0,0 +1,56 @@
+# NixOS Manual {#book-nixos-manual}
+## Version @NIXOS_VERSION@
+
+<!--
+  this is the top-level structure file for the nixos manual.
+
+  the manual structure extends the nixpkgs commonmark further with include
+  blocks to allow better organization of input text. there are six types of
+  include blocks: preface, parts, chapters, sections, appendix, and options.
+  each type except `options`` corresponds to the docbook elements of (roughly)
+  the same name, and can itself can further include blocks to denote its
+  substructure.
+
+  non-`options`` include blocks are fenced code blocks that list a number of
+  files to include, in the form
+
+     ```{=include=} <type>
+     <file-name-1>
+     <file-name-2>
+     <...>
+     ```
+
+  `options` include blocks do not list file names but contain a list of key-value
+  pairs that describe the options to be included and how to convert them into
+  elements of the manual output type:
+
+      ```{=include=} options
+      id-prefix: <options id prefix>
+      list-id: <variable list element id>
+      source: <path to options.json>
+      ```
+
+-->
+
+```{=include=} preface
+preface.md
+```
+
+```{=include=} parts
+installation/installation.md
+configuration/configuration.md
+administration/running.md
+development/development.md
+```
+
+```{=include=} chapters
+contributing-to-this-manual.chapter.md
+```
+
+```{=include=} appendix html:into-file=//options.html
+nixos-options.md
+```
+
+```{=include=} appendix html:into-file=//release-notes.html
+release-notes/release-notes.md
+```
diff --git a/nixos/doc/manual/manual.xml b/nixos/doc/manual/manual.xml
deleted file mode 100644
index 158b3507a58..00000000000
--- a/nixos/doc/manual/manual.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<book 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="book-nixos-manual">
- <info>
-  <title>NixOS Manual</title>
-  <subtitle>Version <xi:include href="./generated/version" parse="text" />
-  </subtitle>
- </info>
- <xi:include href="preface.xml" />
- <xi:include href="installation/installation.xml" />
- <xi:include href="configuration/configuration.xml" />
- <xi:include href="administration/running.xml" />
-<!-- <xi:include href="userconfiguration.xml" /> -->
- <xi:include href="development/development.xml" />
- <appendix xml:id="ch-options">
-  <title>Configuration Options</title>
-  <xi:include href="./generated/options-db.xml"
-                xpointer="configuration-variable-list" />
- </appendix>
- <xi:include href="./from_md/contributing-to-this-manual.chapter.xml" />
- <xi:include href="release-notes/release-notes.xml" />
-</book>
diff --git a/nixos/doc/manual/md-to-db.sh b/nixos/doc/manual/md-to-db.sh
deleted file mode 100755
index beb0ff9f708..00000000000
--- a/nixos/doc/manual/md-to-db.sh
+++ /dev/null
@@ -1,50 +0,0 @@
-#! /usr/bin/env nix-shell
-#! 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
-# into DocBook files in the from_md folder.
-
-DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
-pushd "$DIR"
-
-# NOTE: Keep in sync with Nixpkgs manual (/doc/Makefile).
-# TODO: Remove raw-attribute when we can get rid of DocBook altogether.
-pandoc_commonmark_enabled_extensions=+attributes+fenced_divs+footnotes+bracketed_spans+definition_lists+pipe_tables+raw_attribute
-pandoc_flags=(
-  # Not needed:
-  # - diagram-generator.lua (we do not support that in NixOS manual to limit dependencies)
-  # - media extraction (was only required for diagram generator)
-  # - docbook-reader/citerefentry-to-rst-role.lua (only relevant for DocBook → MarkDown/rST/MyST)
-  "--lua-filter=$DIR/../../../doc/build-aux/pandoc-filters/myst-reader/roles.lua"
-  "--lua-filter=$DIR/../../../doc/build-aux/pandoc-filters/link-unix-man-references.lua"
-  "--lua-filter=$DIR/../../../doc/build-aux/pandoc-filters/docbook-writer/rst-roles.lua"
-  "--lua-filter=$DIR/../../../doc/build-aux/pandoc-filters/docbook-writer/html-elements.lua"
-  "--lua-filter=$DIR/../../../doc/build-aux/pandoc-filters/docbook-writer/labelless-link-is-xref.lua"
-  -f "commonmark${pandoc_commonmark_enabled_extensions}+smart"
-  -t docbook
-)
-
-OUT="$DIR/from_md"
-mapfile -t MD_FILES < <(find . -type f -regex '.*\.md$')
-
-for mf in ${MD_FILES[*]}; do
-  if [ "${mf: -11}" == ".section.md" ]; then
-    mkdir -p "$(dirname "$OUT/$mf")"
-    OUTFILE="$OUT/${mf%".section.md"}.section.xml"
-    pandoc "$mf" "${pandoc_flags[@]}" \
-      -o "$OUTFILE"
-    grep -q -m 1 "xi:include" "$OUTFILE" && sed -i 's|xmlns:xlink="http://www.w3.org/1999/xlink"| xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude"|' "$OUTFILE"
-  fi
-
-  if [ "${mf: -11}" == ".chapter.md" ]; then
-    mkdir -p "$(dirname "$OUT/$mf")"
-    OUTFILE="$OUT/${mf%".chapter.md"}.chapter.xml"
-    pandoc "$mf" "${pandoc_flags[@]}" \
-      --top-level-division=chapter \
-      -o "$OUTFILE"
-    grep -q -m 1 "xi:include" "$OUTFILE" && sed -i 's|xmlns:xlink="http://www.w3.org/1999/xlink"| xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude"|' "$OUTFILE"
-  fi
-done
-
-popd
diff --git a/nixos/doc/manual/nixos-options.md b/nixos/doc/manual/nixos-options.md
new file mode 100644
index 00000000000..33b487c95a2
--- /dev/null
+++ b/nixos/doc/manual/nixos-options.md
@@ -0,0 +1,7 @@
+# Configuration Options {#ch-options}
+
+```{=include=} options
+id-prefix: opt-
+list-id: configuration-variable-list
+source: @NIXOS_OPTIONS_JSON@
+```
diff --git a/nixos/doc/manual/preface.md b/nixos/doc/manual/preface.md
new file mode 100644
index 00000000000..d5e6364780a
--- /dev/null
+++ b/nixos/doc/manual/preface.md
@@ -0,0 +1,11 @@
+# Preface {#preface}
+
+This manual describes how to install, use and extend NixOS, a Linux distribution based on the purely functional package management system [Nix](https://nixos.org/nix), that is composed using modules and packages defined in the [Nixpkgs](https://nixos.org/nixpkgs) project.
+
+Additional information regarding the Nix package manager and the Nixpkgs project can be found in respectively the [Nix manual](https://nixos.org/nix/manual) and the [Nixpkgs manual](https://nixos.org/nixpkgs/manual).
+
+If you encounter problems, please report them on the [`Discourse`](https://discourse.nixos.org), the [Matrix room](https://matrix.to/#nix:nixos.org), or on the [`#nixos` channel on Libera.Chat](irc://irc.libera.chat/#nixos). Alternatively, consider [contributing to this manual](#chap-contributing). Bugs should be reported in [NixOS’ GitHub issue tracker](https://github.com/NixOS/nixpkgs/issues).
+
+::: {.note}
+Commands prefixed with `#` have to be run as root, either requiring to login as root user or temporarily switching to it using `sudo` for example.
+:::
diff --git a/nixos/doc/manual/preface.xml b/nixos/doc/manual/preface.xml
deleted file mode 100644
index c0d530c3d1b..00000000000
--- a/nixos/doc/manual/preface.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<preface xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xml:id="preface">
- <title>Preface</title>
- <para>
-  This manual describes how to install, use and extend NixOS, a Linux
-  distribution based on the purely functional package management system
-  <link xlink:href="https://nixos.org/nix">Nix</link>, that is composed
-  using modules and packages defined in the
-  <link xlink:href="https://nixos.org/nixpkgs">Nixpkgs</link> project.
- </para>
- <para>
-  Additional information regarding the Nix package manager and the Nixpkgs
-  project can be found in respectively the
-  <link xlink:href="https://nixos.org/nix/manual">Nix manual</link> and the
-  <link xlink:href="https://nixos.org/nixpkgs/manual">Nixpkgs manual</link>.
- </para>
- <para>
-  If you encounter problems, please report them on the
-  <literal
-   xlink:href="https://discourse.nixos.org">Discourse</literal>,
-  the <link
-   xlink:href="https://matrix.to/#nix:nixos.org">Matrix room</link>,
-  or on the <link
-   xlink:href="irc://irc.libera.chat/#nixos">
-  <literal>#nixos</literal> channel on Libera.Chat</link>.
-  Alternatively, consider <link
-   xlink:href="#chap-contributing">
-   contributing to this manual</link>. Bugs should be
-  reported in
-  <link
-   xlink:href="https://github.com/NixOS/nixpkgs/issues">NixOS’
-  GitHub issue tracker</link>.
- </para>
- <note>
-  <para>
-   Commands prefixed with <literal>#</literal> have to be run as root, either
-   requiring to login as root user or temporarily switching to it using
-   <literal>sudo</literal> for example.
-  </para>
- </note>
-</preface>
diff --git a/nixos/doc/manual/release-notes/release-notes.md b/nixos/doc/manual/release-notes/release-notes.md
new file mode 100644
index 00000000000..ac61de3793e
--- /dev/null
+++ b/nixos/doc/manual/release-notes/release-notes.md
@@ -0,0 +1,25 @@
+# Release Notes {#ch-release-notes}
+
+This section lists the release notes for each stable version of NixOS and current unstable revision.
+
+```{=include=} sections
+rl-2305.section.md
+rl-2211.section.md
+rl-2205.section.md
+rl-2111.section.md
+rl-2105.section.md
+rl-2009.section.md
+rl-2003.section.md
+rl-1909.section.md
+rl-1903.section.md
+rl-1809.section.md
+rl-1803.section.md
+rl-1709.section.md
+rl-1703.section.md
+rl-1609.section.md
+rl-1603.section.md
+rl-1509.section.md
+rl-1412.section.md
+rl-1404.section.md
+rl-1310.section.md
+```
diff --git a/nixos/doc/manual/release-notes/release-notes.xml b/nixos/doc/manual/release-notes/release-notes.xml
deleted file mode 100644
index bb5cc677afb..00000000000
--- a/nixos/doc/manual/release-notes/release-notes.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<appendix 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="ch-release-notes">
- <title>Release Notes</title>
- <para>
-  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-2305.section.xml" />
- <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" />
- <xi:include href="../from_md/release-notes/rl-2003.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1909.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1903.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1809.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1803.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1709.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1703.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1609.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1603.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1509.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1412.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1404.section.xml" />
- <xi:include href="../from_md/release-notes/rl-1310.section.xml" />
-</appendix>
diff --git a/nixos/doc/manual/release-notes/rl-1509.section.md b/nixos/doc/manual/release-notes/rl-1509.section.md
index 55804ddb988..1422ae4c299 100644
--- a/nixos/doc/manual/release-notes/rl-1509.section.md
+++ b/nixos/doc/manual/release-notes/rl-1509.section.md
@@ -2,7 +2,7 @@
 
 In addition to numerous new and upgraded packages, this release has the following highlights:
 
-- The [Haskell](http://haskell.org/) packages infrastructure has been re-designed from the ground up (\"Haskell NG\"). NixOS now distributes the latest version of every single package registered on [Hackage](http://hackage.haskell.org/) \-- well in excess of 8,000 Haskell packages. Detailed instructions on how to use that infrastructure can be found in the [User\'s Guide to the Haskell Infrastructure](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure). Users migrating from an earlier release may find helpful information below, in the list of backwards-incompatible changes. Furthermore, we distribute 51(!) additional Haskell package sets that provide every single [LTS Haskell](http://www.stackage.org/) release since version 0.0 as well as the most recent [Stackage Nightly](http://www.stackage.org/) snapshot. The announcement [\"Full Stackage Support in Nixpkgs\"](https://nixos.org/nix-dev/2015-September/018138.html) gives additional details.
+- The [Haskell](http://haskell.org/) packages infrastructure has been re-designed from the ground up ("Haskell NG"). NixOS now distributes the latest version of every single package registered on [Hackage](http://hackage.haskell.org/) \-- well in excess of 8,000 Haskell packages. Detailed instructions on how to use that infrastructure can be found in the [User's Guide to the Haskell Infrastructure](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure). Users migrating from an earlier release may find helpful information below, in the list of backwards-incompatible changes. Furthermore, we distribute 51(!) additional Haskell package sets that provide every single [LTS Haskell](http://www.stackage.org/) release since version 0.0 as well as the most recent [Stackage Nightly](http://www.stackage.org/) snapshot. The announcement ["Full Stackage Support in Nixpkgs"](https://nixos.org/nix-dev/2015-September/018138.html) gives additional details.
 
 - Nix has been updated to version 1.10, which among other improvements enables cryptographic signatures on binary caches for improved security.
 
@@ -178,7 +178,7 @@ The new option `system.stateVersion` ensures that certain configuration changes
 
 - Nix now requires binary caches to be cryptographically signed. If you have unsigned binary caches that you want to continue to use, you should set `nix.requireSignedBinaryCaches = false`.
 
-- Steam now doesn\'t need root rights to work. Instead of using `*-steam-chrootenv`, you should now just run `steam`. `steamChrootEnv` package was renamed to `steam`, and old `steam` package \-- to `steamOriginal`.
+- Steam now doesn't need root rights to work. Instead of using `*-steam-chrootenv`, you should now just run `steam`. `steamChrootEnv` package was renamed to `steam`, and old `steam` package \-- to `steamOriginal`.
 
 - CMPlayer has been renamed to bomi upstream. Package `cmplayer` was accordingly renamed to `bomi`
 
@@ -203,7 +203,7 @@ The new option `system.stateVersion` ensures that certain configuration changes
 }
 ```
 
-- \"`nix-env -qa`\" no longer discovers Haskell packages by name. The only packages visible in the global scope are `ghc`, `cabal-install`, and `stack`, but all other packages are hidden. The reason for this inconvenience is the sheer size of the Haskell package set. Name-based lookups are expensive, and most `nix-env -qa` operations would become much slower if we\'d add the entire Hackage database into the top level attribute set. Instead, the list of Haskell packages can be displayed by running:
+- "`nix-env -qa`" no longer discovers Haskell packages by name. The only packages visible in the global scope are `ghc`, `cabal-install`, and `stack`, but all other packages are hidden. The reason for this inconvenience is the sheer size of the Haskell package set. Name-based lookups are expensive, and most `nix-env -qa` operations would become much slower if we'd add the entire Hackage database into the top level attribute set. Instead, the list of Haskell packages can be displayed by running:
 
 ```ShellSession
 nix-env -f "<nixpkgs>" -qaP -A haskellPackages
@@ -217,11 +217,11 @@ nix-env -f "<nixpkgs>" -iA haskellPackages.pandoc
 
 Installing Haskell _libraries_ this way, however, is no longer supported. See the next item for more details.
 
-- Previous versions of NixOS came with a feature called `ghc-wrapper`, a small script that allowed GHC to transparently pick up on libraries installed in the user\'s profile. This feature has been deprecated; `ghc-wrapper` was removed from the distribution. The proper way to register Haskell libraries with the compiler now is the `haskellPackages.ghcWithPackages` function. The [User\'s Guide to the Haskell Infrastructure](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) provides more information about this subject.
+- Previous versions of NixOS came with a feature called `ghc-wrapper`, a small script that allowed GHC to transparently pick up on libraries installed in the user's profile. This feature has been deprecated; `ghc-wrapper` was removed from the distribution. The proper way to register Haskell libraries with the compiler now is the `haskellPackages.ghcWithPackages` function. The [User's Guide to the Haskell Infrastructure](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) provides more information about this subject.
 
 - All Haskell builds that have been generated with version 1.x of the `cabal2nix` utility are now invalid and need to be re-generated with a current version of `cabal2nix` to function. The most recent version of this tool can be installed by running `nix-env -i cabal2nix`.
 
-- The `haskellPackages` set in Nixpkgs used to have a function attribute called `extension` that users could override in their `~/.nixpkgs/config.nix` files to configure additional attributes, etc. That function still exists, but it\'s now called `overrides`.
+- The `haskellPackages` set in Nixpkgs used to have a function attribute called `extension` that users could override in their `~/.nixpkgs/config.nix` files to configure additional attributes, etc. That function still exists, but it's now called `overrides`.
 
 - The OpenBLAS library has been updated to version `0.2.14`. Support for the `x86_64-darwin` platform was added. Dynamic architecture detection was enabled; OpenBLAS now selects microarchitecture-optimized routines at runtime, so optimal performance is achieved without the need to rebuild OpenBLAS locally. OpenBLAS has replaced ATLAS in most packages which use an optimized BLAS or LAPACK implementation.
 
@@ -312,7 +312,7 @@ Other notable improvements:
 
 - The nixos and nixpkgs channels were unified, so one _can_ use `nix-env -iA nixos.bash` instead of `nix-env -iA nixos.pkgs.bash`. See [the commit](https://github.com/NixOS/nixpkgs/commit/2cd7c1f198) for details.
 
-- Users running an SSH server who worry about the quality of their `/etc/ssh/moduli` file with respect to the [vulnerabilities discovered in the Diffie-Hellman key exchange](https://stribika.github.io/2015/01/04/secure-secure-shell.html) can now replace OpenSSH\'s default version with one they generated themselves using the new `services.openssh.moduliFile` option.
+- Users running an SSH server who worry about the quality of their `/etc/ssh/moduli` file with respect to the [vulnerabilities discovered in the Diffie-Hellman key exchange](https://stribika.github.io/2015/01/04/secure-secure-shell.html) can now replace OpenSSH's default version with one they generated themselves using the new `services.openssh.moduliFile` option.
 
 - A newly packaged TeX Live 2015 is provided in `pkgs.texlive`, split into 6500 nix packages. For basic user documentation see [the source](https://github.com/NixOS/nixpkgs/blob/release-15.09/pkgs/tools/typesetting/tex/texlive/default.nix#L1). Beware of [an issue](https://github.com/NixOS/nixpkgs/issues/9757) when installing a too large package set. The plan is to deprecate and maybe delete the original TeX packages until the next release.
 
diff --git a/nixos/doc/manual/release-notes/rl-1603.section.md b/nixos/doc/manual/release-notes/rl-1603.section.md
index e4da7fd3094..532a16f937b 100644
--- a/nixos/doc/manual/release-notes/rl-1603.section.md
+++ b/nixos/doc/manual/release-notes/rl-1603.section.md
@@ -152,19 +152,19 @@ When upgrading from a previous release, please be aware of the following incompa
   }
   ```
 
-- `s3sync` is removed, as it hasn\'t been developed by upstream for 4 years and only runs with ruby 1.8. For an actively-developer alternative look at `tarsnap` and others.
+- `s3sync` is removed, as it hasn't been developed by upstream for 4 years and only runs with ruby 1.8. For an actively-developer alternative look at `tarsnap` and others.
 
-- `ruby_1_8` has been removed as it\'s not supported from upstream anymore and probably contains security issues.
+- `ruby_1_8` has been removed as it's not supported from upstream anymore and probably contains security issues.
 
 - `tidy-html5` package is removed. Upstream only provided `(lib)tidy5` during development, and now they went back to `(lib)tidy` to work as a drop-in replacement of the original package that has been unmaintained for years. You can (still) use the `html-tidy` package, which got updated to a stable release from this new upstream.
 
 - `extraDeviceOptions` argument is removed from `bumblebee` package. Instead there are now two separate arguments: `extraNvidiaDeviceOptions` and `extraNouveauDeviceOptions` for setting extra X11 options for nvidia and nouveau drivers, respectively.
 
-- The `Ctrl+Alt+Backspace` key combination no longer kills the X server by default. There\'s a new option `services.xserver.enableCtrlAltBackspace` allowing to enable the combination again.
+- The `Ctrl+Alt+Backspace` key combination no longer kills the X server by default. There's a new option `services.xserver.enableCtrlAltBackspace` allowing to enable the combination again.
 
 - `emacsPackagesNg` now contains all packages from the ELPA, MELPA, and MELPA Stable repositories.
 
-- Data directory for Postfix MTA server is moved from `/var/postfix` to `/var/lib/postfix`. Old configurations are migrated automatically. `service.postfix` module has also received many improvements, such as correct directories\' access rights, new `aliasFiles` and `mapFiles` options and more.
+- Data directory for Postfix MTA server is moved from `/var/postfix` to `/var/lib/postfix`. Old configurations are migrated automatically. `service.postfix` module has also received many improvements, such as correct directories' access rights, new `aliasFiles` and `mapFiles` options and more.
 
 - Filesystem options should now be configured as a list of strings, not a comma-separated string. The old style will continue to work, but print a warning, until the 16.09 release. An example of the new style:
 
@@ -180,7 +180,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - CUPS, installed by `services.printing` module, now has its data directory in `/var/lib/cups`. Old configurations from `/etc/cups` are moved there automatically, but there might be problems. Also configuration options `services.printing.cupsdConf` and `services.printing.cupsdFilesConf` were removed because they had been allowing one to override configuration variables required for CUPS to work at all on NixOS. For most use cases, `services.printing.extraConf` and new option `services.printing.extraFilesConf` should be enough; if you encounter a situation when they are not, please file a bug.
 
-  There are also Gutenprint improvements; in particular, a new option `services.printing.gutenprint` is added to enable automatic updating of Gutenprint PPMs; it\'s greatly recommended to enable it instead of adding `gutenprint` to the `drivers` list.
+  There are also Gutenprint improvements; in particular, a new option `services.printing.gutenprint` is added to enable automatic updating of Gutenprint PPMs; it's greatly recommended to enable it instead of adding `gutenprint` to the `drivers` list.
 
 - `services.xserver.vaapiDrivers` has been removed. Use `hardware.opengl.extraPackages{,32}` instead. You can also specify VDPAU drivers there.
 
@@ -202,7 +202,7 @@ When upgrading from a previous release, please be aware of the following incompa
   }
   ```
 
-- `services.udev.extraRules` option now writes rules to `99-local.rules` instead of `10-local.rules`. This makes all the user rules apply after others, so their results wouldn\'t be overridden by anything else.
+- `services.udev.extraRules` option now writes rules to `99-local.rules` instead of `10-local.rules`. This makes all the user rules apply after others, so their results wouldn't be overridden by anything else.
 
 - Large parts of the `services.gitlab` module has been been rewritten. There are new configuration options available. The `stateDir` option was renamned to `statePath` and the `satellitesDir` option was removed. Please review the currently available options.
 
@@ -246,7 +246,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
   you should either re-run `nixos-generate-config` or manually replace `"${config.boot.kernelPackages.broadcom_sta}"` by `config.boot.kernelPackages.broadcom_sta` in your `/etc/nixos/hardware-configuration.nix`. More discussion is on [ the github issue](https://github.com/NixOS/nixpkgs/pull/12595).
 
-- The `services.xserver.startGnuPGAgent` option has been removed. GnuPG 2.1.x changed the way the gpg-agent works, and that new approach no longer requires (or even supports) the \"start everything as a child of the agent\" scheme we\'ve implemented in NixOS for older versions. To configure the gpg-agent for your X session, add the following code to `~/.bashrc` or some file that's sourced when your shell is started:
+- The `services.xserver.startGnuPGAgent` option has been removed. GnuPG 2.1.x changed the way the gpg-agent works, and that new approach no longer requires (or even supports) the "start everything as a child of the agent" scheme we've implemented in NixOS for older versions. To configure the gpg-agent for your X session, add the following code to `~/.bashrc` or some file that's sourced when your shell is started:
 
   ```shell
   GPG_TTY=$(tty)
@@ -273,7 +273,7 @@ When upgrading from a previous release, please be aware of the following incompa
       gpg --import ~/.gnupg/secring.gpg
   ```
 
-  The `gpg-agent(1)` man page has more details about this subject, i.e. in the \"EXAMPLES\" section.
+  The `gpg-agent(1)` man page has more details about this subject, i.e. in the "EXAMPLES" section.
 
 Other notable improvements:
 
diff --git a/nixos/doc/manual/release-notes/rl-1609.section.md b/nixos/doc/manual/release-notes/rl-1609.section.md
index 075f0cf52cd..e9c650cf407 100644
--- a/nixos/doc/manual/release-notes/rl-1609.section.md
+++ b/nixos/doc/manual/release-notes/rl-1609.section.md
@@ -20,7 +20,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - A large number of packages have been converted to use the multiple outputs feature of Nix to greatly reduce the amount of required disk space, as mentioned above. This may require changes to any custom packages to make them build again; see the relevant chapter in the Nixpkgs manual for more information. (Additional caveat to packagers: some packaging conventions related to multiple-output packages [were changed](https://github.com/NixOS/nixpkgs/pull/14766) late (August 2016) in the release cycle and differ from the initial introduction of multiple outputs.)
 
-- Previous versions of Nixpkgs had support for all versions of the LTS Haskell package set. That support has been dropped. The previously provided `haskell.packages.lts-x_y` package sets still exist in name to aviod breaking user code, but these package sets don\'t actually contain the versions mandated by the corresponding LTS release. Instead, our package set it loosely based on the latest available LTS release, i.e. LTS 7.x at the time of this writing. New releases of NixOS and Nixpkgs will drop those old names entirely. [The motivation for this change](https://nixos.org/nix-dev/2016-June/020585.html) has been discussed at length on the `nix-dev` mailing list and in [Github issue \#14897](https://github.com/NixOS/nixpkgs/issues/14897). Development strategies for Haskell hackers who want to rely on Nix and NixOS have been described in [another nix-dev article](https://nixos.org/nix-dev/2016-June/020642.html).
+- Previous versions of Nixpkgs had support for all versions of the LTS Haskell package set. That support has been dropped. The previously provided `haskell.packages.lts-x_y` package sets still exist in name to aviod breaking user code, but these package sets don't actually contain the versions mandated by the corresponding LTS release. Instead, our package set it loosely based on the latest available LTS release, i.e. LTS 7.x at the time of this writing. New releases of NixOS and Nixpkgs will drop those old names entirely. [The motivation for this change](https://nixos.org/nix-dev/2016-June/020585.html) has been discussed at length on the `nix-dev` mailing list and in [Github issue \#14897](https://github.com/NixOS/nixpkgs/issues/14897). Development strategies for Haskell hackers who want to rely on Nix and NixOS have been described in [another nix-dev article](https://nixos.org/nix-dev/2016-June/020642.html).
 
 - Shell aliases for systemd sub-commands [were dropped](https://github.com/NixOS/nixpkgs/pull/15598): `start`, `stop`, `restart`, `status`.
 
@@ -28,7 +28,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - `/var/empty` is now immutable. Activation script runs `chattr +i` to forbid any modifications inside the folder. See [ the pull request](https://github.com/NixOS/nixpkgs/pull/18365) for what bugs this caused.
 
-- Gitlab\'s maintainance script `gitlab-runner` was removed and split up into the more clearer `gitlab-run` and `gitlab-rake` scripts, because `gitlab-runner` is a component of Gitlab CI.
+- Gitlab's maintainance script `gitlab-runner` was removed and split up into the more clearer `gitlab-run` and `gitlab-rake` scripts, because `gitlab-runner` is a component of Gitlab CI.
 
 - `services.xserver.libinput.accelProfile` default changed from `flat` to `adaptive`, as per [ official documentation](https://wayland.freedesktop.org/libinput/doc/latest/group__config.html#gad63796972347f318b180e322e35cee79).
 
@@ -38,7 +38,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - `pkgs.linuxPackages.virtualbox` now contains only the kernel modules instead of the VirtualBox user space binaries. If you want to reference the user space binaries, you have to use the new `pkgs.virtualbox` instead.
 
-- `goPackages` was replaced with separated Go applications in appropriate `nixpkgs` categories. Each Go package uses its own dependency set. There\'s also a new `go2nix` tool introduced to generate a Go package definition from its Go source automatically.
+- `goPackages` was replaced with separated Go applications in appropriate `nixpkgs` categories. Each Go package uses its own dependency set. There's also a new `go2nix` tool introduced to generate a Go package definition from its Go source automatically.
 
 - `services.mongodb.extraConfig` configuration format was changed to YAML.
 
diff --git a/nixos/doc/manual/release-notes/rl-1703.section.md b/nixos/doc/manual/release-notes/rl-1703.section.md
index 7f424f2a6ce..b82c41e28ca 100644
--- a/nixos/doc/manual/release-notes/rl-1703.section.md
+++ b/nixos/doc/manual/release-notes/rl-1703.section.md
@@ -8,7 +8,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - This release is based on Glibc 2.25, GCC 5.4.0 and systemd 232. The default Linux kernel is 4.9 and Nix is at 1.11.8.
 
-- The default desktop environment now is KDE\'s Plasma 5. KDE 4 has been removed
+- The default desktop environment now is KDE's Plasma 5. KDE 4 has been removed
 
 - The setuid wrapper functionality now supports setting capabilities.
 
@@ -208,7 +208,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - Two lone top-level dict dbs moved into `dictdDBs`. This affects: `dictdWordnet` which is now at `dictdDBs.wordnet` and `dictdWiktionary` which is now at `dictdDBs.wiktionary`
 
-- Parsoid service now uses YAML configuration format. `service.parsoid.interwikis` is now called `service.parsoid.wikis` and is a list of either API URLs or attribute sets as specified in parsoid\'s documentation.
+- Parsoid service now uses YAML configuration format. `service.parsoid.interwikis` is now called `service.parsoid.wikis` and is a list of either API URLs or attribute sets as specified in parsoid's documentation.
 
 - `Ntpd` was replaced by `systemd-timesyncd` as the default service to synchronize system time with a remote NTP server. The old behavior can be restored by setting `services.ntp.enable` to `true`. Upstream time servers for all NTP implementations are now configured using `networking.timeServers`.
 
@@ -260,11 +260,11 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - Autoloading connection tracking helpers is now disabled by default. This default was also changed in the Linux kernel and is considered insecure if not configured properly in your firewall. If you need connection tracking helpers (i.e. for active FTP) please enable `networking.firewall.autoLoadConntrackHelpers` and tune `networking.firewall.connectionTrackingModules` to suit your needs.
 
-- `local_recipient_maps` is not set to empty value by Postfix service. It\'s an insecure default as stated by Postfix documentation. Those who want to retain this setting need to set it via `services.postfix.extraConfig`.
+- `local_recipient_maps` is not set to empty value by Postfix service. It's an insecure default as stated by Postfix documentation. Those who want to retain this setting need to set it via `services.postfix.extraConfig`.
 
 - Iputils no longer provide ping6 and traceroute6. The functionality of these tools has been integrated into ping and traceroute respectively. To enforce an address family the new flags `-4` and `-6` have been added. One notable incompatibility is that specifying an interface (for link-local IPv6 for instance) is no longer done with the `-I` flag, but by encoding the interface into the address (`ping fe80::1%eth0`).
 
-- The socket handling of the `services.rmilter` module has been fixed and refactored. As rmilter doesn\'t support binding to more than one socket, the options `bindUnixSockets` and `bindInetSockets` have been replaced by `services.rmilter.bindSocket.*`. The default is still a unix socket in `/run/rmilter/rmilter.sock`. Refer to the options documentation for more information.
+- The socket handling of the `services.rmilter` module has been fixed and refactored. As rmilter doesn't support binding to more than one socket, the options `bindUnixSockets` and `bindInetSockets` have been replaced by `services.rmilter.bindSocket.*`. The default is still a unix socket in `/run/rmilter/rmilter.sock`. Refer to the options documentation for more information.
 
 - The `fetch*` functions no longer support md5, please use sha256 instead.
 
@@ -278,7 +278,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - Module type system have a new extensible option types feature that allow to extend certain types, such as enum, through multiple option declarations of the same option across multiple modules.
 
-- `jre` now defaults to GTK UI by default. This improves visual consistency and makes Java follow system font style, improving the situation on HighDPI displays. This has a cost of increased closure size; for server and other headless workloads it\'s recommended to use `jre_headless`.
+- `jre` now defaults to GTK UI by default. This improves visual consistency and makes Java follow system font style, improving the situation on HighDPI displays. This has a cost of increased closure size; for server and other headless workloads it's recommended to use `jre_headless`.
 
 - Python 2.6 interpreter and package set have been removed.
 
diff --git a/nixos/doc/manual/release-notes/rl-1709.section.md b/nixos/doc/manual/release-notes/rl-1709.section.md
index 970a0c2b7dd..9f49549901b 100644
--- a/nixos/doc/manual/release-notes/rl-1709.section.md
+++ b/nixos/doc/manual/release-notes/rl-1709.section.md
@@ -8,7 +8,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - The user handling now keeps track of deallocated UIDs/GIDs. When a user or group is revived, this allows it to be allocated the UID/GID it had before. A consequence is that UIDs and GIDs are no longer reused.
 
-- The module option `services.xserver.xrandrHeads` now causes the first head specified in this list to be set as the primary head. Apart from that, it\'s now possible to also set additional options by using an attribute set, for example:
+- The module option `services.xserver.xrandrHeads` now causes the first head specified in this list to be set as the primary head. Apart from that, it's now possible to also set additional options by using an attribute set, for example:
 
   ```nix
   { services.xserver.xrandrHeads = [
@@ -208,7 +208,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
   - The `mysql` default `dataDir` has changed from `/var/mysql` to `/var/lib/mysql`.
 
-  - Radicale\'s default package has changed from 1.x to 2.x. Instructions to migrate can be found [ here ](http://radicale.org/1to2/). It is also possible to use the newer version by setting the `package` to `radicale2`, which is done automatically when `stateVersion` is 17.09 or higher. The `extraArgs` option has been added to allow passing the data migration arguments specified in the instructions; see the `radicale.nix` NixOS test for an example migration.
+  - Radicale's default package has changed from 1.x to 2.x. Instructions to migrate can be found [ here ](http://radicale.org/1to2/). It is also possible to use the newer version by setting the `package` to `radicale2`, which is done automatically when `stateVersion` is 17.09 or higher. The `extraArgs` option has been added to allow passing the data migration arguments specified in the instructions; see the `radicale.nix` NixOS test for an example migration.
 
 - The `aiccu` package was removed. This is due to SixXS [ sunsetting](https://www.sixxs.net/main/) its IPv6 tunnel.
 
@@ -216,9 +216,9 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - Top-level `idea` package collection was renamed. All JetBrains IDEs are now at `jetbrains`.
 
-- `flexget`\'s state database cannot be upgraded to its new internal format, requiring removal of any existing `db-config.sqlite` which will be automatically recreated.
+- `flexget`'s state database cannot be upgraded to its new internal format, requiring removal of any existing `db-config.sqlite` which will be automatically recreated.
 
-- The `ipfs` service now doesn\'t ignore the `dataDir` option anymore. If you\'ve ever set this option to anything other than the default you\'ll have to either unset it (so the default gets used) or migrate the old data manually with
+- The `ipfs` service now doesn't ignore the `dataDir` option anymore. If you've ever set this option to anything other than the default you'll have to either unset it (so the default gets used) or migrate the old data manually with
 
   ```ShellSession
   dataDir=<valueOfDataDir>
@@ -236,7 +236,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - `wvdial` package and module were removed. This is due to the project being dead and not building with openssl 1.1.
 
-- `cc-wrapper`\'s setup-hook now exports a number of environment variables corresponding to binutils binaries, (e.g. `LD`, `STRIP`, `RANLIB`, etc). This is done to prevent packages\' build systems guessing, which is harder to predict, especially when cross-compiling. However, some packages have broken due to this---their build systems either not supporting, or claiming to support without adequate testing, taking such environment variables as parameters.
+- `cc-wrapper`'s setup-hook now exports a number of environment variables corresponding to binutils binaries, (e.g. `LD`, `STRIP`, `RANLIB`, etc). This is done to prevent packages' build systems guessing, which is harder to predict, especially when cross-compiling. However, some packages have broken due to this---their build systems either not supporting, or claiming to support without adequate testing, taking such environment variables as parameters.
 
 - `services.firefox.syncserver` now runs by default as a non-root user. To accommodate this change, the default sqlite database location has also been changed. Migration should work automatically. Refer to the description of the options for more details.
 
@@ -244,7 +244,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - Touchpad support should now be enabled through `libinput` as `synaptics` is now deprecated. See the option `services.xserver.libinput.enable`.
 
-- grsecurity/PaX support has been dropped, following upstream\'s decision to cease free support. See [ upstream\'s announcement](https://grsecurity.net/passing_the_baton.php) for more information. No complete replacement for grsecurity/PaX is available presently.
+- grsecurity/PaX support has been dropped, following upstream's decision to cease free support. See [ upstream's announcement](https://grsecurity.net/passing_the_baton.php) for more information. No complete replacement for grsecurity/PaX is available presently.
 
 - `services.mysql` now has declarative configuration of databases and users with the `ensureDatabases` and `ensureUsers` options.
 
@@ -283,9 +283,9 @@ When upgrading from a previous release, please be aware of the following incompa
 
 ## Other Notable Changes {#sec-release-17.09-notable-changes}
 
-- Modules can now be disabled by using [ disabledModules](https://nixos.org/nixpkgs/manual/#sec-replace-modules), allowing another to take it\'s place. This can be used to import a set of modules from another channel while keeping the rest of the system on a stable release.
+- Modules can now be disabled by using [ disabledModules](https://nixos.org/nixpkgs/manual/#sec-replace-modules), allowing another to take it's place. This can be used to import a set of modules from another channel while keeping the rest of the system on a stable release.
 
-- Updated to FreeType 2.7.1, including a new TrueType engine. The new engine replaces the Infinality engine which was the default in NixOS. The default font rendering settings are now provided by fontconfig-penultimate, replacing fontconfig-ultimate; the new defaults are less invasive and provide rendering that is more consistent with other systems and hopefully with each font designer\'s intent. Some system-wide configuration has been removed from the Fontconfig NixOS module where user Fontconfig settings are available.
+- Updated to FreeType 2.7.1, including a new TrueType engine. The new engine replaces the Infinality engine which was the default in NixOS. The default font rendering settings are now provided by fontconfig-penultimate, replacing fontconfig-ultimate; the new defaults are less invasive and provide rendering that is more consistent with other systems and hopefully with each font designer's intent. Some system-wide configuration has been removed from the Fontconfig NixOS module where user Fontconfig settings are available.
 
 - ZFS/SPL have been updated to 0.7.0, `zfsUnstable, splUnstable` have therefore been removed.
 
diff --git a/nixos/doc/manual/release-notes/rl-1803.section.md b/nixos/doc/manual/release-notes/rl-1803.section.md
index c5146015d44..681894eb13e 100644
--- a/nixos/doc/manual/release-notes/rl-1803.section.md
+++ b/nixos/doc/manual/release-notes/rl-1803.section.md
@@ -6,7 +6,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - End of support is planned for end of October 2018, handing over to 18.09.
 
-- Platform support: x86_64-linux and x86_64-darwin since release time (the latter isn\'t NixOS, really). Binaries for aarch64-linux are available, but no channel exists yet, as it\'s waiting for some test fixes, etc.
+- Platform support: x86_64-linux and x86_64-darwin since release time (the latter isn't NixOS, really). Binaries for aarch64-linux are available, but no channel exists yet, as it's waiting for some test fixes, etc.
 
 - Nix now defaults to 2.0; see its [release notes](https://nixos.org/nix/manual/#ssec-relnotes-2.0).
 
@@ -176,7 +176,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - `cc-wrapper` has been split in two; there is now also a `bintools-wrapper`. The most commonly used files in `nix-support` are now split between the two wrappers. Some commonly used ones, like `nix-support/dynamic-linker`, are duplicated for backwards compatability, even though they rightly belong only in `bintools-wrapper`. Other more obscure ones are just moved.
 
-- The propagation logic has been changed. The new logic, along with new types of dependencies that go with, is thoroughly documented in the \"Specifying dependencies\" section of the \"Standard Environment\" chapter of the nixpkgs manual. The old logic isn\'t but is easy to describe: dependencies were propagated as the same type of dependency no matter what. In practice, that means that many `propagatedNativeBuildInputs` should instead be `propagatedBuildInputs`. Thankfully, that was and is the least used type of dependency. Also, it means that some `propagatedBuildInputs` should instead be `depsTargetTargetPropagated`. Other types dependencies should be unaffected.
+- The propagation logic has been changed. The new logic, along with new types of dependencies that go with, is thoroughly documented in the "Specifying dependencies" section of the "Standard Environment" chapter of the nixpkgs manual. The old logic isn't but is easy to describe: dependencies were propagated as the same type of dependency no matter what. In practice, that means that many `propagatedNativeBuildInputs` should instead be `propagatedBuildInputs`. Thankfully, that was and is the least used type of dependency. Also, it means that some `propagatedBuildInputs` should instead be `depsTargetTargetPropagated`. Other types dependencies should be unaffected.
 
 - `lib.addPassthru drv passthru` is removed. Use `lib.extendDerivation true passthru drv` instead.
 
@@ -184,7 +184,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The `hardware.amdHybridGraphics.disable` option was removed for lack of a maintainer. If you still need this module, you may wish to include a copy of it from an older version of nixos in your imports.
 
-- The merging of config options for `services.postfix.config` was buggy. Previously, if other options in the Postfix module like `services.postfix.useSrs` were set and the user set config options that were also set by such options, the resulting config wouldn\'t include all options that were needed. They are now merged correctly. If config options need to be overridden, `lib.mkForce` or `lib.mkOverride` can be used.
+- The merging of config options for `services.postfix.config` was buggy. Previously, if other options in the Postfix module like `services.postfix.useSrs` were set and the user set config options that were also set by such options, the resulting config wouldn't include all options that were needed. They are now merged correctly. If config options need to be overridden, `lib.mkForce` or `lib.mkOverride` can be used.
 
 - The following changes apply if the `stateVersion` is changed to 18.03 or higher. For `stateVersion = "17.09"` or lower the old behavior is preserved.
 
@@ -204,7 +204,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
   - The data directory `/var/lib/piwik` was renamed to `/var/lib/matomo`. All files will be moved automatically on first startup, but you might need to adjust your backup scripts.
 
-  - The default `serverName` for the nginx configuration changed from `piwik.${config.networking.hostName}` to `matomo.${config.networking.hostName}.${config.networking.domain}` if `config.networking.domain` is set, `matomo.${config.networking.hostName}` if it is not set. If you change your `serverName`, remember you\'ll need to update the `trustedHosts[]` array in `/var/lib/matomo/config/config.ini.php` as well.
+  - The default `serverName` for the nginx configuration changed from `piwik.${config.networking.hostName}` to `matomo.${config.networking.hostName}.${config.networking.domain}` if `config.networking.domain` is set, `matomo.${config.networking.hostName}` if it is not set. If you change your `serverName`, remember you'll need to update the `trustedHosts[]` array in `/var/lib/matomo/config/config.ini.php` as well.
 
   - The `piwik` user was renamed to `matomo`. The service will adjust ownership automatically for files in the data directory. If you use unix socket authentication, remember to give the new `matomo` user access to the database and to change the `username` to `matomo` in the `[database]` section of `/var/lib/matomo/config/config.ini.php`.
 
@@ -250,7 +250,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The option `services.logstash.listenAddress` is now `127.0.0.1` by default. Previously the default behaviour was to listen on all interfaces.
 
-- `services.btrfs.autoScrub` has been added, to periodically check btrfs filesystems for data corruption. If there\'s a correct copy available, it will automatically repair corrupted blocks.
+- `services.btrfs.autoScrub` has been added, to periodically check btrfs filesystems for data corruption. If there's a correct copy available, it will automatically repair corrupted blocks.
 
 - `displayManager.lightdm.greeters.gtk.clock-format.` has been added, the clock format string (as expected by strftime, e.g. `%H:%M`) to use with the lightdm gtk greeter panel.
 
diff --git a/nixos/doc/manual/release-notes/rl-1809.section.md b/nixos/doc/manual/release-notes/rl-1809.section.md
index 3443db37c97..71afc71d5a8 100644
--- a/nixos/doc/manual/release-notes/rl-1809.section.md
+++ b/nixos/doc/manual/release-notes/rl-1809.section.md
@@ -204,11 +204,11 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The `clementine` package points now to the free derivation. `clementineFree` is removed now and `clementineUnfree` points to the package which is bundled with the unfree `libspotify` package.
 
-- The `netcat` package is now taken directly from OpenBSD\'s `libressl`, instead of relying on Debian\'s fork. The new version should be very close to the old version, but there are some minor differences. Importantly, flags like -b, -q, -C, and -Z are no longer accepted by the nc command.
+- The `netcat` package is now taken directly from OpenBSD's `libressl`, instead of relying on Debian's fork. The new version should be very close to the old version, but there are some minor differences. Importantly, flags like -b, -q, -C, and -Z are no longer accepted by the nc command.
 
-- The `services.docker-registry.extraConfig` object doesn\'t contain environment variables anymore. Instead it needs to provide an object structure that can be mapped onto the YAML configuration defined in [the `docker/distribution` docs](https://github.com/docker/distribution/blob/v2.6.2/docs/configuration.md).
+- The `services.docker-registry.extraConfig` object doesn't contain environment variables anymore. Instead it needs to provide an object structure that can be mapped onto the YAML configuration defined in [the `docker/distribution` docs](https://github.com/docker/distribution/blob/v2.6.2/docs/configuration.md).
 
-- `gnucash` has changed from version 2.4 to 3.x. If you\'ve been using `gnucash` (version 2.4) instead of `gnucash26` (version 2.6) you must open your Gnucash data file(s) with `gnucash26` and then save them to upgrade the file format. Then you may use your data file(s) with Gnucash 3.x. See the upgrade [documentation](https://wiki.gnucash.org/wiki/FAQ#Using_Different_Versions.2C_Up_And_Downgrade). Gnucash 2.4 is still available under the attribute `gnucash24`.
+- `gnucash` has changed from version 2.4 to 3.x. If you've been using `gnucash` (version 2.4) instead of `gnucash26` (version 2.6) you must open your Gnucash data file(s) with `gnucash26` and then save them to upgrade the file format. Then you may use your data file(s) with Gnucash 3.x. See the upgrade [documentation](https://wiki.gnucash.org/wiki/FAQ#Using_Different_Versions.2C_Up_And_Downgrade). Gnucash 2.4 is still available under the attribute `gnucash24`.
 
 - `services.munge` now runs as user (and group) `munge` instead of root. Make sure the key file is accessible to the daemon.
 
@@ -315,7 +315,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The Kubernetes Dashboard now has only minimal RBAC permissions by default. If dashboard cluster-admin rights are desired, set `services.kubernetes.addons.dashboard.rbac.clusterAdmin` to true. On existing clusters, in order for the revocation of privileges to take effect, the current ClusterRoleBinding for kubernetes-dashboard must be manually removed: `kubectl delete clusterrolebinding kubernetes-dashboard`
 
-- The `programs.screen` module provides allows to configure `/etc/screenrc`, however the module behaved fairly counterintuitive as the config exists, but the package wasn\'t available. Since 18.09 `pkgs.screen` will be added to `environment.systemPackages`.
+- The `programs.screen` module provides allows to configure `/etc/screenrc`, however the module behaved fairly counterintuitive as the config exists, but the package wasn't available. Since 18.09 `pkgs.screen` will be added to `environment.systemPackages`.
 
 - The module `services.networking.hostapd` now uses WPA2 by default.
 
@@ -327,6 +327,6 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The default display manager is now LightDM. To use SLiM set `services.xserver.displayManager.slim.enable` to `true`.
 
-- NixOS option descriptions are now automatically broken up into individual paragraphs if the text contains two consecutive newlines, so it\'s no longer necessary to use `</para><para>` to start a new paragraph.
+- NixOS option descriptions are now automatically broken up into individual paragraphs if the text contains two consecutive newlines, so it's no longer necessary to use `</para><para>` to start a new paragraph.
 
 - Top-level `buildPlatform`, `hostPlatform`, and `targetPlatform` in Nixpkgs are deprecated. Please use their equivalents in `stdenv` instead: `stdenv.buildPlatform`, `stdenv.hostPlatform`, and `stdenv.targetPlatform`.
diff --git a/nixos/doc/manual/release-notes/rl-1903.section.md b/nixos/doc/manual/release-notes/rl-1903.section.md
index e560b9f3044..b43518c471f 100644
--- a/nixos/doc/manual/release-notes/rl-1903.section.md
+++ b/nixos/doc/manual/release-notes/rl-1903.section.md
@@ -11,11 +11,11 @@ In addition to numerous new and upgraded packages, this release has the followin
 - Added the Pantheon desktop environment. It can be enabled through `services.xserver.desktopManager.pantheon.enable`.
 
   ::: {.note}
-  By default, `services.xserver.desktopManager.pantheon` enables LightDM as a display manager, as pantheon\'s screen locking implementation relies on it.
-  Because of that it is recommended to leave LightDM enabled. If you\'d like to disable it anyway, set `services.xserver.displayManager.lightdm.enable` to `false` and enable your preferred display manager.
+  By default, `services.xserver.desktopManager.pantheon` enables LightDM as a display manager, as pantheon's screen locking implementation relies on it.
+  Because of that it is recommended to leave LightDM enabled. If you'd like to disable it anyway, set `services.xserver.displayManager.lightdm.enable` to `false` and enable your preferred display manager.
   :::
 
-  Also note that Pantheon\'s LightDM greeter is not enabled by default, because it has numerous issues in NixOS and isn\'t optimal for use here yet.
+  Also note that Pantheon's LightDM greeter is not enabled by default, because it has numerous issues in NixOS and isn't optimal for use here yet.
 
 - A major refactoring of the Kubernetes module has been completed. Refactorings primarily focus on decoupling components and enhancing security. Two-way TLS and RBAC has been enabled by default for all components, which slightly changes the way the module is configured. See: [](#sec-kubernetes) for details.
 
@@ -57,7 +57,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The Syncthing state and configuration data has been moved from `services.syncthing.dataDir` to the newly defined `services.syncthing.configDir`, which default to `/var/lib/syncthing/.config/syncthing`. This change makes possible to share synced directories using ACLs without Syncthing resetting the permission on every start.
 
-- The `ntp` module now has sane default restrictions. If you\'re relying on the previous defaults, which permitted all queries and commands from all firewall-permitted sources, you can set `services.ntp.restrictDefault` and `services.ntp.restrictSource` to `[]`.
+- The `ntp` module now has sane default restrictions. If you're relying on the previous defaults, which permitted all queries and commands from all firewall-permitted sources, you can set `services.ntp.restrictDefault` and `services.ntp.restrictSource` to `[]`.
 
 - Package `rabbitmq_server` is renamed to `rabbitmq-server`.
 
@@ -89,9 +89,9 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The option `services.xserver.displayManager.job.logToFile` which was previously set to `true` when using the display managers `lightdm`, `sddm` or `xpra` has been reset to the default value (`false`).
 
-- Network interface indiscriminate NixOS firewall options (`networking.firewall.allow*`) are now preserved when also setting interface specific rules such as `networking.firewall.interfaces.en0.allow*`. These rules continue to use the pseudo device \"default\" (`networking.firewall.interfaces.default.*`), and assigning to this pseudo device will override the (`networking.firewall.allow*`) options.
+- Network interface indiscriminate NixOS firewall options (`networking.firewall.allow*`) are now preserved when also setting interface specific rules such as `networking.firewall.interfaces.en0.allow*`. These rules continue to use the pseudo device "default" (`networking.firewall.interfaces.default.*`), and assigning to this pseudo device will override the (`networking.firewall.allow*`) options.
 
-- The `nscd` service now disables all caching of `passwd` and `group` databases by default. This was interferring with the correct functioning of the `libnss_systemd.so` module which is used by `systemd` to manage uids and usernames in the presence of `DynamicUser=` in systemd services. This was already the default behaviour in presence of `services.sssd.enable = true` because nscd caching would interfere with `sssd` in unpredictable ways as well. Because we\'re using nscd not for caching, but for convincing glibc to find NSS modules in the nix store instead of an absolute path, we have decided to disable caching globally now, as it\'s usually not the behaviour the user wants and can lead to surprising behaviour. Furthermore, negative caching of host lookups is also disabled now by default. This should fix the issue of dns lookups failing in the presence of an unreliable network.
+- The `nscd` service now disables all caching of `passwd` and `group` databases by default. This was interferring with the correct functioning of the `libnss_systemd.so` module which is used by `systemd` to manage uids and usernames in the presence of `DynamicUser=` in systemd services. This was already the default behaviour in presence of `services.sssd.enable = true` because nscd caching would interfere with `sssd` in unpredictable ways as well. Because we're using nscd not for caching, but for convincing glibc to find NSS modules in the nix store instead of an absolute path, we have decided to disable caching globally now, as it's usually not the behaviour the user wants and can lead to surprising behaviour. Furthermore, negative caching of host lookups is also disabled now by default. This should fix the issue of dns lookups failing in the presence of an unreliable network.
 
   If the old behaviour is desired, this can be restored by setting the `services.nscd.config` option with the desired caching parameters.
 
@@ -137,7 +137,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The `pam_unix` account module is now loaded with its control field set to `required` instead of `sufficient`, so that later PAM account modules that might do more extensive checks are being executed. Previously, the whole account module verification was exited prematurely in case a nss module provided the account name to `pam_unix`. The LDAP and SSSD NixOS modules already add their NSS modules when enabled. In case your setup breaks due to some later PAM account module previosuly shadowed, or failing NSS lookups, please file a bug. You can get back the old behaviour by manually setting `security.pam.services.<name?>.text`.
 
-- The `pam_unix` password module is now loaded with its control field set to `sufficient` instead of `required`, so that password managed only by later PAM password modules are being executed. Previously, for example, changing an LDAP account\'s password through PAM was not possible: the whole password module verification was exited prematurely by `pam_unix`, preventing `pam_ldap` to manage the password as it should.
+- The `pam_unix` password module is now loaded with its control field set to `sufficient` instead of `required`, so that password managed only by later PAM password modules are being executed. Previously, for example, changing an LDAP account's password through PAM was not possible: the whole password module verification was exited prematurely by `pam_unix`, preventing `pam_ldap` to manage the password as it should.
 
 - `fish` has been upgraded to 3.0. It comes with a number of improvements and backwards incompatible changes. See the `fish` [release notes](https://github.com/fish-shell/fish-shell/releases/tag/3.0.0) for more information.
 
@@ -145,7 +145,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - NixOS module system type `types.optionSet` and `lib.mkOption` argument `options` are deprecated. Use `types.submodule` instead. ([\#54637](https://github.com/NixOS/nixpkgs/pull/54637))
 
-- `matrix-synapse` has been updated to version 0.99. It will [no longer generate a self-signed certificate on first launch](https://github.com/matrix-org/synapse/pull/4509) and will be [the last version to accept self-signed certificates](https://matrix.org/blog/2019/02/05/synapse-0-99-0/). As such, it is now recommended to use a proper certificate verified by a root CA (for example Let\'s Encrypt). The new [manual chapter on Matrix](#module-services-matrix) contains a working example of using nginx as a reverse proxy in front of `matrix-synapse`, using Let\'s Encrypt certificates.
+- `matrix-synapse` has been updated to version 0.99. It will [no longer generate a self-signed certificate on first launch](https://github.com/matrix-org/synapse/pull/4509) and will be [the last version to accept self-signed certificates](https://matrix.org/blog/2019/02/05/synapse-0-99-0/). As such, it is now recommended to use a proper certificate verified by a root CA (for example Let's Encrypt). The new [manual chapter on Matrix](#module-services-matrix) contains a working example of using nginx as a reverse proxy in front of `matrix-synapse`, using Let's Encrypt certificates.
 
 - `mailutils` now works by default when `sendmail` is not in a setuid wrapper. As a consequence, the `sendmailPath` argument, having lost its main use, has been removed.
 
@@ -191,7 +191,7 @@ When upgrading from a previous release, please be aware of the following incompa
   With this change application specific volumes are relative to the master volume which can be adjusted independently, whereas before they were absolute; meaning that in effect, it scaled the device-volume with the volume of the loudest application.
   :::
 
-- The [`ndppd`](https://github.com/DanielAdolfsson/ndppd) module now supports [all config options](options.html#opt-services.ndppd.enable) provided by the current upstream version as service options. Additionally the `ndppd` package doesn\'t contain the systemd unit configuration from upstream anymore, the unit is completely configured by the NixOS module now.
+- The [`ndppd`](https://github.com/DanielAdolfsson/ndppd) module now supports [all config options](options.html#opt-services.ndppd.enable) provided by the current upstream version as service options. Additionally the `ndppd` package doesn't contain the systemd unit configuration from upstream anymore, the unit is completely configured by the NixOS module now.
 
 - New installs of NixOS will default to the Redmine 4.x series unless otherwise specified in `services.redmine.package` while existing installs of NixOS will default to the Redmine 3.x series.
 
diff --git a/nixos/doc/manual/release-notes/rl-1909.section.md b/nixos/doc/manual/release-notes/rl-1909.section.md
index 0f09f9b9273..42835238819 100644
--- a/nixos/doc/manual/release-notes/rl-1909.section.md
+++ b/nixos/doc/manual/release-notes/rl-1909.section.md
@@ -34,7 +34,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - The installer now uses a less privileged `nixos` user whereas before we logged in as root. To gain root privileges use `sudo -i` without a password.
 
-- We\'ve updated to Xfce 4.14, which brings a new module `services.xserver.desktopManager.xfce4-14`. If you\'d like to upgrade, please switch from the `services.xserver.desktopManager.xfce` module as it will be deprecated in a future release. They\'re incompatibilities with the current Xfce module; it doesn\'t support `thunarPlugins` and it isn\'t recommended to use `services.xserver.desktopManager.xfce` and `services.xserver.desktopManager.xfce4-14` simultaneously or to downgrade from Xfce 4.14 after upgrading.
+- We've updated to Xfce 4.14, which brings a new module `services.xserver.desktopManager.xfce4-14`. If you'd like to upgrade, please switch from the `services.xserver.desktopManager.xfce` module as it will be deprecated in a future release. They're incompatibilities with the current Xfce module; it doesn't support `thunarPlugins` and it isn't recommended to use `services.xserver.desktopManager.xfce` and `services.xserver.desktopManager.xfce4-14` simultaneously or to downgrade from Xfce 4.14 after upgrading.
 
 - The GNOME 3 desktop manager module sports an interface to enable/disable core services, applications, and optional GNOME packages like games.
 
@@ -46,9 +46,9 @@ In addition to numerous new and upgraded packages, this release has the followin
 
   - `services.gnome3.games.enable`
 
-  With these options we hope to give users finer grained control over their systems. Prior to this change you\'d either have to manually disable options or use `environment.gnome3.excludePackages` which only excluded the optional applications. `environment.gnome3.excludePackages` is now unguarded, it can exclude any package installed with `environment.systemPackages` in the GNOME 3 module.
+  With these options we hope to give users finer grained control over their systems. Prior to this change you'd either have to manually disable options or use `environment.gnome3.excludePackages` which only excluded the optional applications. `environment.gnome3.excludePackages` is now unguarded, it can exclude any package installed with `environment.systemPackages` in the GNOME 3 module.
 
-- Orthogonal to the previous changes to the GNOME 3 desktop manager module, we\'ve updated all default services and applications to match as close as possible to a default reference GNOME 3 experience.
+- Orthogonal to the previous changes to the GNOME 3 desktop manager module, we've updated all default services and applications to match as close as possible to a default reference GNOME 3 experience.
 
   **The following changes were enacted in `services.gnome3.core-utilities.enable`**
 
@@ -104,7 +104,7 @@ The following new services were added since the last release:
 
   - `services.xserver.desktopManager.pantheon`
 
-  - `services.xserver.desktopManager.mate` Note Mate uses `programs.system-config-printer` as it doesn\'t use it as a service, but its graphical interface directly.
+  - `services.xserver.desktopManager.mate` Note Mate uses `programs.system-config-printer` as it doesn't use it as a service, but its graphical interface directly.
 
 - [services.blueman.enable](options.html#opt-services.blueman.enable) has been added. If you previously had blueman installed via `environment.systemPackages` please migrate to using the NixOS module, as this would result in an insufficiently configured blueman.
 
@@ -118,11 +118,11 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - PostgreSQL 9.4 is scheduled EOL during the 19.09 life cycle and has been removed.
 
-- The options `services.prometheus.alertmanager.user` and `services.prometheus.alertmanager.group` have been removed because the alertmanager service is now using systemd\'s [ DynamicUser mechanism](http://0pointer.net/blog/dynamic-users-with-systemd.html) which obviates these options.
+- The options `services.prometheus.alertmanager.user` and `services.prometheus.alertmanager.group` have been removed because the alertmanager service is now using systemd's [ DynamicUser mechanism](http://0pointer.net/blog/dynamic-users-with-systemd.html) which obviates these options.
 
 - The NetworkManager systemd unit was renamed back from network-manager.service to NetworkManager.service for better compatibility with other applications expecting this name. The same applies to ModemManager where modem-manager.service is now called ModemManager.service again.
 
-- The `services.nzbget.configFile` and `services.nzbget.openFirewall` options were removed as they are managed internally by the nzbget. The `services.nzbget.dataDir` option hadn\'t actually been used by the module for some time and so was removed as cleanup.
+- The `services.nzbget.configFile` and `services.nzbget.openFirewall` options were removed as they are managed internally by the nzbget. The `services.nzbget.dataDir` option hadn't actually been used by the module for some time and so was removed as cleanup.
 
 - The `services.mysql.pidDir` option was removed, as it was only used by the wordpress apache-httpd service to wait for mysql to have started up. This can be accomplished by either describing a dependency on mysql.service (preferred) or waiting for the (hardcoded) `/run/mysqld/mysql.sock` file to appear.
 
@@ -148,7 +148,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
   A new knob named `nixops.enableDeprecatedAutoLuks` has been introduced to disable the eval failure and to acknowledge the notice was received and read. If you plan on using the feature please note that it might break with subsequent updates.
 
-  Make sure you set the `_netdev` option for each of the file systems referring to block devices provided by the autoLuks module. Not doing this might render the system in a state where it doesn\'t boot anymore.
+  Make sure you set the `_netdev` option for each of the file systems referring to block devices provided by the autoLuks module. Not doing this might render the system in a state where it doesn't boot anymore.
 
   If you are actively using the `autoLuks` module please let us know in [issue \#62211](https://github.com/NixOS/nixpkgs/issues/62211).
 
@@ -196,13 +196,13 @@ When upgrading from a previous release, please be aware of the following incompa
 
   Furthermore, the acme module will not automatically add a dependency on `lighttpd.service` anymore. If you are using certficates provided by letsencrypt for lighttpd, then you should depend on the certificate service `acme-${cert}.service>` manually.
 
-  For nginx, the dependencies are still automatically managed when `services.nginx.virtualhosts.<name>.enableACME` is enabled just like before. What changed is that nginx now directly depends on the specific certificates that it needs, instead of depending on the catch-all `acme-certificates.target`. This target unit was also removed from the codebase. This will mean nginx will no longer depend on certificates it isn\'t explicitly managing and fixes a bug with certificate renewal ordering racing with nginx restarting which could lead to nginx getting in a broken state as described at [NixOS/nixpkgs\#60180](https://github.com/NixOS/nixpkgs/issues/60180).
+  For nginx, the dependencies are still automatically managed when `services.nginx.virtualhosts.<name>.enableACME` is enabled just like before. What changed is that nginx now directly depends on the specific certificates that it needs, instead of depending on the catch-all `acme-certificates.target`. This target unit was also removed from the codebase. This will mean nginx will no longer depend on certificates it isn't explicitly managing and fixes a bug with certificate renewal ordering racing with nginx restarting which could lead to nginx getting in a broken state as described at [NixOS/nixpkgs\#60180](https://github.com/NixOS/nixpkgs/issues/60180).
 
 - The old deprecated `emacs` package sets have been dropped. What used to be called `emacsPackagesNg` is now simply called `emacsPackages`.
 
-- `services.xserver.desktopManager.xterm` is now disabled by default if `stateVersion` is 19.09 or higher. Previously the xterm desktopManager was enabled when xserver was enabled, but it isn\'t useful for all people so it didn\'t make sense to have any desktopManager enabled default.
+- `services.xserver.desktopManager.xterm` is now disabled by default if `stateVersion` is 19.09 or higher. Previously the xterm desktopManager was enabled when xserver was enabled, but it isn't useful for all people so it didn't make sense to have any desktopManager enabled default.
 
-- The WeeChat plugin `pkgs.weechatScripts.weechat-xmpp` has been removed as it doesn\'t receive any updates from upstream and depends on outdated Python2-based modules.
+- The WeeChat plugin `pkgs.weechatScripts.weechat-xmpp` has been removed as it doesn't receive any updates from upstream and depends on outdated Python2-based modules.
 
 - Old unsupported versions (`logstash5`, `kibana5`, `filebeat5`, `heartbeat5`, `metricbeat5`, `packetbeat5`) of the ELK-stack and Elastic beats have been removed.
 
@@ -210,7 +210,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - Citrix Receiver (`citrix_receiver`) has been dropped in favor of Citrix Workspace (`citrix_workspace`).
 
-- The `services.gitlab` module has had its literal secret options (`services.gitlab.smtp.password`, `services.gitlab.databasePassword`, `services.gitlab.initialRootPassword`, `services.gitlab.secrets.secret`, `services.gitlab.secrets.db`, `services.gitlab.secrets.otp` and `services.gitlab.secrets.jws`) replaced by file-based versions (`services.gitlab.smtp.passwordFile`, `services.gitlab.databasePasswordFile`, `services.gitlab.initialRootPasswordFile`, `services.gitlab.secrets.secretFile`, `services.gitlab.secrets.dbFile`, `services.gitlab.secrets.otpFile` and `services.gitlab.secrets.jwsFile`). This was done so that secrets aren\'t stored in the world-readable nix store, but means that for each option you\'ll have to create a file with the same exact string, add \"File\" to the end of the option name, and change the definition to a string pointing to the corresponding file; e.g. `services.gitlab.databasePassword = "supersecurepassword"` becomes `services.gitlab.databasePasswordFile = "/path/to/secret_file"` where the file `secret_file` contains the string `supersecurepassword`.
+- The `services.gitlab` module has had its literal secret options (`services.gitlab.smtp.password`, `services.gitlab.databasePassword`, `services.gitlab.initialRootPassword`, `services.gitlab.secrets.secret`, `services.gitlab.secrets.db`, `services.gitlab.secrets.otp` and `services.gitlab.secrets.jws`) replaced by file-based versions (`services.gitlab.smtp.passwordFile`, `services.gitlab.databasePasswordFile`, `services.gitlab.initialRootPasswordFile`, `services.gitlab.secrets.secretFile`, `services.gitlab.secrets.dbFile`, `services.gitlab.secrets.otpFile` and `services.gitlab.secrets.jwsFile`). This was done so that secrets aren't stored in the world-readable nix store, but means that for each option you'll have to create a file with the same exact string, add "File" to the end of the option name, and change the definition to a string pointing to the corresponding file; e.g. `services.gitlab.databasePassword = "supersecurepassword"` becomes `services.gitlab.databasePasswordFile = "/path/to/secret_file"` where the file `secret_file` contains the string `supersecurepassword`.
 
   The state path (`services.gitlab.statePath`) now has the following restriction: no parent directory can be owned by any other user than `root` or the user specified in `services.gitlab.user`; i.e. if `services.gitlab.statePath` is set to `/var/lib/gitlab/state`, `gitlab` and all parent directories must be owned by either `root` or the user specified in `services.gitlab.user`.
 
@@ -218,7 +218,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The Twitter client `corebird` has been dropped as [it is discontinued and does not work against the new Twitter API](https://www.patreon.com/posts/corebirds-future-18921328). Please use the fork `cawbird` instead which has been adapted to the API changes and is still maintained.
 
-- The `nodejs-11_x` package has been removed as it\'s EOLed by upstream.
+- The `nodejs-11_x` package has been removed as it's EOLed by upstream.
 
 - Because of the systemd upgrade, systemd-timesyncd will no longer work if `system.stateVersion` is not set correctly. When upgrading from NixOS 19.03, please make sure that `system.stateVersion` is set to `"19.03"`, or lower if the installation dates back to an earlier version of NixOS.
 
@@ -252,7 +252,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The `consul` package was upgraded past version `1.5`, so its deprecated legacy UI is no longer available.
 
-- The default resample-method for PulseAudio has been changed from the upstream default `speex-float-1` to `speex-float-5`. Be aware that low-powered ARM-based and MIPS-based boards will struggle with this so you\'ll need to set `hardware.pulseaudio.daemon.config.resample-method` back to `speex-float-1`.
+- The default resample-method for PulseAudio has been changed from the upstream default `speex-float-1` to `speex-float-5`. Be aware that low-powered ARM-based and MIPS-based boards will struggle with this so you'll need to set `hardware.pulseaudio.daemon.config.resample-method` back to `speex-float-1`.
 
 - The `phabricator` package and associated `httpd.extraSubservice`, as well as the `phd` service have been removed from nixpkgs due to lack of maintainer.
 
@@ -264,7 +264,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The `tomcat-connector` `httpd.extraSubservice` has been removed from nixpkgs.
 
-- It\'s now possible to change configuration in [services.nextcloud](options.html#opt-services.nextcloud.enable) after the initial deploy since all config parameters are persisted in an additional config file generated by the module. Previously core configuration like database parameters were set using their imperative installer after creating `/var/lib/nextcloud`.
+- It's now possible to change configuration in [services.nextcloud](options.html#opt-services.nextcloud.enable) after the initial deploy since all config parameters are persisted in an additional config file generated by the module. Previously core configuration like database parameters were set using their imperative installer after creating `/var/lib/nextcloud`.
 
 - There exists now `lib.forEach`, which is like `map`, but with arguments flipped. When mapping function body spans many lines (or has nested `map`s), it is often hard to follow which list is modified.
 
@@ -308,6 +308,6 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The `altcoins` categorization of packages has been removed. You now access these packages at the top level, ie. `nix-shell -p dogecoin` instead of `nix-shell -p altcoins.dogecoin`, etc.
 
-- Ceph has been upgraded to v14.2.1. See the [release notes](https://ceph.com/releases/v14-2-0-nautilus-released/) for details. The mgr dashboard as well as osds backed by loop-devices is no longer explicitly supported by the package and module. Note: There\'s been some issues with python-cherrypy, which is used by the dashboard and prometheus mgr modules (and possibly others), hence 0000-dont-check-cherrypy-version.patch.
+- Ceph has been upgraded to v14.2.1. See the [release notes](https://ceph.com/releases/v14-2-0-nautilus-released/) for details. The mgr dashboard as well as osds backed by loop-devices is no longer explicitly supported by the package and module. Note: There's been some issues with python-cherrypy, which is used by the dashboard and prometheus mgr modules (and possibly others), hence 0000-dont-check-cherrypy-version.patch.
 
 - `pkgs.weechat` is now compiled against `pkgs.python3`. Weechat also recommends [to use Python3 in their docs.](https://weechat.org/scripts/python3/)
diff --git a/nixos/doc/manual/release-notes/rl-2003.section.md b/nixos/doc/manual/release-notes/rl-2003.section.md
index b92c7f6634c..76cee8858e8 100644
--- a/nixos/doc/manual/release-notes/rl-2003.section.md
+++ b/nixos/doc/manual/release-notes/rl-2003.section.md
@@ -34,11 +34,11 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - Postgresql for NixOS service now defaults to v11.
 
-- The graphical installer image starts the graphical session automatically. Before you\'d be greeted by a tty and asked to enter `systemctl start display-manager`. It is now possible to disable the display-manager from running by selecting the `Disable display-manager` quirk in the boot menu.
+- The graphical installer image starts the graphical session automatically. Before you'd be greeted by a tty and asked to enter `systemctl start display-manager`. It is now possible to disable the display-manager from running by selecting the `Disable display-manager` quirk in the boot menu.
 
 - GNOME 3 has been upgraded to 3.34. Please take a look at their [Release Notes](https://help.gnome.org/misc/release-notes/3.34) for details.
 
-- If you enable the Pantheon Desktop Manager via [services.xserver.desktopManager.pantheon.enable](options.html#opt-services.xserver.desktopManager.pantheon.enable), we now default to also use [ Pantheon\'s newly designed greeter ](https://blog.elementary.io/say-hello-to-the-new-greeter/). Contrary to NixOS\'s usual update policy, Pantheon will receive updates during the cycle of NixOS 20.03 when backwards compatible.
+- If you enable the Pantheon Desktop Manager via [services.xserver.desktopManager.pantheon.enable](options.html#opt-services.xserver.desktopManager.pantheon.enable), we now default to also use [ Pantheon's newly designed greeter ](https://blog.elementary.io/say-hello-to-the-new-greeter/). Contrary to NixOS's usual update policy, Pantheon will receive updates during the cycle of NixOS 20.03 when backwards compatible.
 
 - By default zfs pools will now be trimmed on a weekly basis. Trimming is only done on supported devices (i.e. NVME or SSDs) and should improve throughput and lifetime of these devices. It is controlled by the `services.zfs.trim.enable` varname. The zfs scrub service (`services.zfs.autoScrub.enable`) and the zfs autosnapshot service (`services.zfs.autoSnapshot.enable`) are now only enabled if zfs is set in `config.boot.initrd.supportedFilesystems` or `config.boot.supportedFilesystems`. These lists will automatically contain zfs as soon as any zfs mountpoint is configured in `fileSystems`.
 
@@ -77,7 +77,7 @@ The following new services were added since the last release:
 
 - The kubernetes kube-proxy now supports a new hostname configuration `services.kubernetes.proxy.hostname` which has to be set if the hostname of the node should be non default.
 
-- UPower\'s configuration is now managed by NixOS and can be customized via `services.upower`.
+- UPower's configuration is now managed by NixOS and can be customized via `services.upower`.
 
 - To use Geary you should enable [programs.geary.enable](options.html#opt-programs.geary.enable) instead of just adding it to [environment.systemPackages](options.html#opt-environment.systemPackages). It was created so Geary could function properly outside of GNOME.
 
@@ -187,9 +187,9 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The `99-main.network` file was removed. Matching all network interfaces caused many breakages, see [\#18962](https://github.com/NixOS/nixpkgs/pull/18962) and [\#71106](https://github.com/NixOS/nixpkgs/pull/71106).
 
-  We already don\'t support the global [networking.useDHCP](options.html#opt-networking.useDHCP), [networking.defaultGateway](options.html#opt-networking.defaultGateway) and [networking.defaultGateway6](options.html#opt-networking.defaultGateway6) options if [networking.useNetworkd](options.html#opt-networking.useNetworkd) is enabled, but direct users to configure the per-device [networking.interfaces.\<name\>....](options.html#opt-networking.interfaces) options.
+  We already don't support the global [networking.useDHCP](options.html#opt-networking.useDHCP), [networking.defaultGateway](options.html#opt-networking.defaultGateway) and [networking.defaultGateway6](options.html#opt-networking.defaultGateway6) options if [networking.useNetworkd](options.html#opt-networking.useNetworkd) is enabled, but direct users to configure the per-device [networking.interfaces.\<name\>....](options.html#opt-networking.interfaces) options.
 
-- The stdenv now runs all bash with `set -u`, to catch the use of undefined variables. Before, it itself used `set -u` but was careful to unset it so other packages\' code ran as before. Now, all bash code is held to the same high standard, and the rather complex stateful manipulation of the options can be discarded.
+- The stdenv now runs all bash with `set -u`, to catch the use of undefined variables. Before, it itself used `set -u` but was careful to unset it so other packages' code ran as before. Now, all bash code is held to the same high standard, and the rather complex stateful manipulation of the options can be discarded.
 
 - The SLIM Display Manager has been removed, as it has been unmaintained since 2013. Consider migrating to a different display manager such as LightDM (current default in NixOS), SDDM, GDM, or using the startx module which uses Xinitrc.
 
@@ -197,7 +197,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The BEAM package set has been deleted. You will only find there the different interpreters. You should now use the different build tools coming with the languages with sandbox mode disabled.
 
-- There is now only one Xfce package-set and module. This means that attributes `xfce4-14` and `xfceUnstable` all now point to the latest Xfce 4.14 packages. And in the future NixOS releases will be the latest released version of Xfce available at the time of the release\'s development (if viable).
+- There is now only one Xfce package-set and module. This means that attributes `xfce4-14` and `xfceUnstable` all now point to the latest Xfce 4.14 packages. And in the future NixOS releases will be the latest released version of Xfce available at the time of the release's development (if viable).
 
 - The [phpfpm](options.html#opt-services.phpfpm.pools) module now sets `PrivateTmp=true` in its systemd units for better process isolation. If you rely on `/tmp` being shared with other services, explicitly override this by setting `serviceConfig.PrivateTmp` to `false` for each phpfpm unit.
 
@@ -221,7 +221,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The packages `openobex` and `obexftp` are no longer installed when enabling Bluetooth via `hardware.bluetooth.enable`.
 
-- The `dump1090` derivation has been changed to use FlightAware\'s dump1090 as its upstream. However, this version does not have an internal webserver anymore. The assets in the `share/dump1090` directory of the derivation can be used in conjunction with an external webserver to replace this functionality.
+- The `dump1090` derivation has been changed to use FlightAware's dump1090 as its upstream. However, this version does not have an internal webserver anymore. The assets in the `share/dump1090` directory of the derivation can be used in conjunction with an external webserver to replace this functionality.
 
 - The fourStore and fourStoreEndpoint modules have been removed.
 
@@ -291,7 +291,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
   - `services.buildkite-agent.meta-data` has been renamed to [services.buildkite-agents.\<name\>.tags](options.html#opt-services.buildkite-agents), to match upstreams naming for 3.x. Its type has also changed - it now accepts an attrset of strings.
 
-  - The`services.buildkite-agent.openssh.publicKeyPath` option has been removed, as it\'s not necessary to deploy public keys to clone private repositories.
+  - The`services.buildkite-agent.openssh.publicKeyPath` option has been removed, as it's not necessary to deploy public keys to clone private repositories.
 
   - `services.buildkite-agent.openssh.privateKeyPath` has been renamed to [buildkite-agents.\<name\>.privateSshKeyPath](options.html#opt-services.buildkite-agents), as the whole `openssh` now only contained that single option.
 
@@ -301,7 +301,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The `gcc5` and `gfortran5` packages have been removed.
 
-- The `services.xserver.displayManager.auto` module has been removed. It was only intended for use in internal NixOS tests, and gave the false impression of it being a special display manager when it\'s actually LightDM. Please use the `services.xserver.displayManager.lightdm.autoLogin` options instead, or any other display manager in NixOS as they all support auto-login. If you used this module specifically because it permitted root auto-login you can override the lightdm-autologin pam module like:
+- The `services.xserver.displayManager.auto` module has been removed. It was only intended for use in internal NixOS tests, and gave the false impression of it being a special display manager when it's actually LightDM. Please use the `services.xserver.displayManager.lightdm.autoLogin` options instead, or any other display manager in NixOS as they all support auto-login. If you used this module specifically because it permitted root auto-login you can override the lightdm-autologin pam module like:
 
   ```nix
   {
@@ -325,13 +325,13 @@ When upgrading from a previous release, please be aware of the following incompa
   auth required pam_succeed_if.so quiet
   ```
 
-  line, where default it\'s:
+  line, where default it's:
 
   ```
    auth required pam_succeed_if.so uid >= 1000 quiet
   ```
 
-  not permitting users with uid\'s below 1000 (like root). All other display managers in NixOS are configured like this.
+  not permitting users with uid's below 1000 (like root). All other display managers in NixOS are configured like this.
 
 - There have been lots of improvements to the Mailman module. As a result,
 
@@ -357,9 +357,9 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - Rspamd was updated to version 2.2. Read [ the upstream migration notes](https://rspamd.com/doc/migration.html#migration-to-rspamd-20) carefully. Please be especially aware that some modules were removed and the default Bayes backend is now Redis.
 
-- The `*psu` versions of oraclejdk8 have been removed as they aren\'t provided by upstream anymore.
+- The `*psu` versions of oraclejdk8 have been removed as they aren't provided by upstream anymore.
 
-- The `services.dnscrypt-proxy` module has been removed as it used the deprecated version of dnscrypt-proxy. We\'ve added [services.dnscrypt-proxy2.enable](options.html#opt-services.dnscrypt-proxy2.enable) to use the supported version. This module supports configuration via the Nix attribute set [services.dnscrypt-proxy2.settings](options.html#opt-services.dnscrypt-proxy2.settings), or by passing a TOML configuration file via [services.dnscrypt-proxy2.configFile](options.html#opt-services.dnscrypt-proxy2.configFile).
+- The `services.dnscrypt-proxy` module has been removed as it used the deprecated version of dnscrypt-proxy. We've added [services.dnscrypt-proxy2.enable](options.html#opt-services.dnscrypt-proxy2.enable) to use the supported version. This module supports configuration via the Nix attribute set [services.dnscrypt-proxy2.settings](options.html#opt-services.dnscrypt-proxy2.settings), or by passing a TOML configuration file via [services.dnscrypt-proxy2.configFile](options.html#opt-services.dnscrypt-proxy2.configFile).
 
   ```nix
   {
@@ -382,13 +382,13 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - `qesteidutil` has been deprecated in favor of `qdigidoc`.
 
-- sqldeveloper_18 has been removed as it\'s not maintained anymore, sqldeveloper has been updated to version `19.4`. Please note that this means that this means that the oraclejdk is now required. For further information please read the [release notes](https://www.oracle.com/technetwork/developer-tools/sql-developer/downloads/sqldev-relnotes-194-5908846.html).
+- sqldeveloper_18 has been removed as it's not maintained anymore, sqldeveloper has been updated to version `19.4`. Please note that this means that this means that the oraclejdk is now required. For further information please read the [release notes](https://www.oracle.com/technetwork/developer-tools/sql-developer/downloads/sqldev-relnotes-194-5908846.html).
 
-- Haskell `env` and `shellFor` dev shell environments now organize dependencies the same way as regular builds. In particular, rather than receiving all the different lists of dependencies mashed together as one big list, and then partitioning into Haskell and non-Hakell dependencies, they work from the original many different dependency parameters and don\'t need to algorithmically partition anything.
+- Haskell `env` and `shellFor` dev shell environments now organize dependencies the same way as regular builds. In particular, rather than receiving all the different lists of dependencies mashed together as one big list, and then partitioning into Haskell and non-Hakell dependencies, they work from the original many different dependency parameters and don't need to algorithmically partition anything.
 
   This means that if you incorrectly categorize a dependency, e.g. non-Haskell library dependency as a `buildDepends` or run-time Haskell dependency as a `setupDepends`, whereas things would have worked before they may not work now.
 
-- The gcc-snapshot-package has been removed. It\'s marked as broken for \>2 years and used to point to a fairly old snapshot from the gcc7-branch.
+- The gcc-snapshot-package has been removed. It's marked as broken for \>2 years and used to point to a fairly old snapshot from the gcc7-branch.
 
 - The nixos-build-vms8 -script now uses the python test-driver.
 
@@ -398,21 +398,21 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - Stand-alone usage of `Upower` now requires `services.upower.enable` instead of just installing into [environment.systemPackages](options.html#opt-environment.systemPackages).
 
-- nextcloud has been updated to `v18.0.2`. This means that users from NixOS 19.09 can\'t upgrade directly since you can only move one version forward and 19.09 uses `v16.0.8`.
+- nextcloud has been updated to `v18.0.2`. This means that users from NixOS 19.09 can't upgrade directly since you can only move one version forward and 19.09 uses `v16.0.8`.
 
   To provide a safe upgrade-path and to circumvent similar issues in the future, the following measures were taken:
 
   - The pkgs.nextcloud-attribute has been removed and replaced with versioned attributes (currently pkgs.nextcloud17 and pkgs.nextcloud18). With this change major-releases can be backported without breaking stuff and to make upgrade-paths easier.
 
-  - Existing setups will be detected using [system.stateVersion](options.html#opt-system.stateVersion): by default, nextcloud17 will be used, but will raise a warning which notes that after that deploy it\'s recommended to update to the latest stable version (nextcloud18) by declaring the newly introduced setting [services.nextcloud.package](options.html#opt-services.nextcloud.package).
+  - Existing setups will be detected using [system.stateVersion](options.html#opt-system.stateVersion): by default, nextcloud17 will be used, but will raise a warning which notes that after that deploy it's recommended to update to the latest stable version (nextcloud18) by declaring the newly introduced setting [services.nextcloud.package](options.html#opt-services.nextcloud.package).
 
-  - Users with an overlay (e.g. to use nextcloud at version `v18` on `19.09`) will get an evaluation error by default. This is done to ensure that our [package](options.html#opt-services.nextcloud.package)-option doesn\'t select an older version by accident. It\'s recommended to use pkgs.nextcloud18 or to set [package](options.html#opt-services.nextcloud.package) to pkgs.nextcloud explicitly.
+  - Users with an overlay (e.g. to use nextcloud at version `v18` on `19.09`) will get an evaluation error by default. This is done to ensure that our [package](options.html#opt-services.nextcloud.package)-option doesn't select an older version by accident. It's recommended to use pkgs.nextcloud18 or to set [package](options.html#opt-services.nextcloud.package) to pkgs.nextcloud explicitly.
 
   ::: {.warning}
-  Please note that if you\'re coming from `19.03` or older, you have to manually upgrade to `19.09` first to upgrade your server to Nextcloud v16.
+  Please note that if you're coming from `19.03` or older, you have to manually upgrade to `19.09` first to upgrade your server to Nextcloud v16.
   :::
 
-- Hydra has gained a massive performance improvement due to [some database schema changes](https://github.com/NixOS/hydra/pull/710) by adding several IDs and better indexing. However, it\'s necessary to upgrade Hydra in multiple steps:
+- Hydra has gained a massive performance improvement due to [some database schema changes](https://github.com/NixOS/hydra/pull/710) by adding several IDs and better indexing. However, it's necessary to upgrade Hydra in multiple steps:
 
   - At first, an older version of Hydra needs to be deployed which adds those (nullable) columns. When having set [stateVersion ](options.html#opt-system.stateVersion) to a value older than `20.03`, this package will be selected by default from the module when upgrading. Otherwise, the package can be deployed using the following config:
 
@@ -434,13 +434,13 @@ When upgrading from a previous release, please be aware of the following incompa
 - Deploy a newer version of Hydra to activate the DB optimizations. This can be done by using hydra-unstable. This package already includes [flake-support](https://github.com/nixos/rfcs/pull/49) and is therefore compiled against pkgs.nixFlakes.
 
   ::: {.warning}
-  If your [stateVersion](options.html#opt-system.stateVersion) is set to `20.03` or greater, hydra-unstable will be used automatically! This will break your setup if you didn\'t run the migration.
+  If your [stateVersion](options.html#opt-system.stateVersion) is set to `20.03` or greater, hydra-unstable will be used automatically! This will break your setup if you didn't run the migration.
   :::
 
-  Please note that Hydra is currently not available with nixStable as this doesn\'t compile anymore.
+  Please note that Hydra is currently not available with nixStable as this doesn't compile anymore.
 
   ::: {.warning}
-  pkgs.hydra has been removed to ensure a graceful database-migration using the dedicated package-attributes. If you still have pkgs.hydra defined in e.g. an overlay, an assertion error will be thrown. To circumvent this, you need to set [services.hydra.package](options.html#opt-services.hydra.package) to pkgs.hydra explicitly and make sure you know what you\'re doing!
+  pkgs.hydra has been removed to ensure a graceful database-migration using the dedicated package-attributes. If you still have pkgs.hydra defined in e.g. an overlay, an assertion error will be thrown. To circumvent this, you need to set [services.hydra.package](options.html#opt-services.hydra.package) to pkgs.hydra explicitly and make sure you know what you're doing!
   :::
 
 - The TokuDB storage engine will be disabled in mariadb 10.5. It is recommended to switch to RocksDB. See also [TokuDB](https://mariadb.com/kb/en/tokudb/).
@@ -478,9 +478,9 @@ When upgrading from a previous release, please be aware of the following incompa
 
   Depending on your setup, you need to incorporate one of the following changes in your setup to upgrade to 20.03:
 
-  - If you use `sqlite3` you don\'t need to do anything.
+  - If you use `sqlite3` you don't need to do anything.
 
-  - If you use `postgresql` on a different server, you don\'t need to change anything as well since this module was never designed to configure remote databases.
+  - If you use `postgresql` on a different server, you don't need to change anything as well since this module was never designed to configure remote databases.
 
   - If you use `postgresql` and configured your synapse initially on `19.09` or older, you simply need to enable postgresql-support explicitly:
 
@@ -496,12 +496,12 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - If you deploy a fresh matrix-synapse, you need to configure the database yourself (e.g. by using the [services.postgresql.initialScript](options.html#opt-services.postgresql.initialScript) option). An example for this can be found in the [documentation of the Matrix module](#module-services-matrix).
 
-- If you initially deployed your matrix-synapse on `nixos-unstable` _after_ the `19.09`-release, your database is misconfigured due to a regression in NixOS. For now, matrix-synapse will startup with a warning, but it\'s recommended to reconfigure the database to set the values `LC_COLLATE` and `LC_CTYPE` to [`'C'`](https://www.postgresql.org/docs/12/locale.html).
+- If you initially deployed your matrix-synapse on `nixos-unstable` _after_ the `19.09`-release, your database is misconfigured due to a regression in NixOS. For now, matrix-synapse will startup with a warning, but it's recommended to reconfigure the database to set the values `LC_COLLATE` and `LC_CTYPE` to [`'C'`](https://www.postgresql.org/docs/12/locale.html).
 
-- The [systemd.network.links](options.html#opt-systemd.network.links) option is now respected even when [systemd-networkd](options.html#opt-systemd.network.enable) is disabled. This mirrors the behaviour of systemd - It\'s udev that parses `.link` files, not `systemd-networkd`.
+- The [systemd.network.links](options.html#opt-systemd.network.links) option is now respected even when [systemd-networkd](options.html#opt-systemd.network.enable) is disabled. This mirrors the behaviour of systemd - It's udev that parses `.link` files, not `systemd-networkd`.
 
 - mongodb has been updated to version `3.4.24`.
 
   ::: {.warning}
-  Please note that mongodb has been relicensed under their own [` sspl`](https://www.mongodb.com/licensing/server-side-public-license/faq)-license. Since it\'s not entirely free and not OSI-approved, it\'s listed as non-free. This means that Hydra doesn\'t provide prebuilt mongodb-packages and needs to be built locally.
+  Please note that mongodb has been relicensed under their own [` sspl`](https://www.mongodb.com/licensing/server-side-public-license/faq)-license. Since it's not entirely free and not OSI-approved, it's listed as non-free. This means that Hydra doesn't provide prebuilt mongodb-packages and needs to be built locally.
   :::
diff --git a/nixos/doc/manual/release-notes/rl-2009.section.md b/nixos/doc/manual/release-notes/rl-2009.section.md
index 79be2a56a54..6995ef1d406 100644
--- a/nixos/doc/manual/release-notes/rl-2009.section.md
+++ b/nixos/doc/manual/release-notes/rl-2009.section.md
@@ -218,7 +218,7 @@ In addition to 1119 new, 118 updated, and 476 removed options; 61 new modules we
 
 When upgrading from a previous release, please be aware of the following incompatible changes:
 
-- MariaDB has been updated to 10.4, MariaDB Galera to 26.4. Before you upgrade, it would be best to take a backup of your database. For MariaDB Galera Cluster, see [Upgrading from MariaDB 10.3 to MariaDB 10.4 with Galera Cluster](https://mariadb.com/kb/en/upgrading-from-mariadb-103-to-mariadb-104-with-galera-cluster/) instead. Before doing the upgrade read [Incompatible Changes Between 10.3 and 10.4](https://mariadb.com/kb/en/upgrading-from-mariadb-103-to-mariadb-104/#incompatible-changes-between-103-and-104). After the upgrade you will need to run `mysql_upgrade`. MariaDB 10.4 introduces a number of changes to the authentication process, intended to make things easier and more intuitive. See [Authentication from MariaDB 10.4](https://mariadb.com/kb/en/authentication-from-mariadb-104/). unix_socket auth plugin does not use a password, and uses the connecting user\'s UID instead. When a new MariaDB data directory is initialized, two MariaDB users are created and can be used with new unix_socket auth plugin, as well as traditional mysql_native_password plugin: root\@localhost and mysql\@localhost. To actually use the traditional mysql_native_password plugin method, one must run the following:
+- MariaDB has been updated to 10.4, MariaDB Galera to 26.4. Before you upgrade, it would be best to take a backup of your database. For MariaDB Galera Cluster, see [Upgrading from MariaDB 10.3 to MariaDB 10.4 with Galera Cluster](https://mariadb.com/kb/en/upgrading-from-mariadb-103-to-mariadb-104-with-galera-cluster/) instead. Before doing the upgrade read [Incompatible Changes Between 10.3 and 10.4](https://mariadb.com/kb/en/upgrading-from-mariadb-103-to-mariadb-104/#incompatible-changes-between-103-and-104). After the upgrade you will need to run `mysql_upgrade`. MariaDB 10.4 introduces a number of changes to the authentication process, intended to make things easier and more intuitive. See [Authentication from MariaDB 10.4](https://mariadb.com/kb/en/authentication-from-mariadb-104/). unix_socket auth plugin does not use a password, and uses the connecting user's UID instead. When a new MariaDB data directory is initialized, two MariaDB users are created and can be used with new unix_socket auth plugin, as well as traditional mysql_native_password plugin: root\@localhost and mysql\@localhost. To actually use the traditional mysql_native_password plugin method, one must run the following:
 
   ```nix
   {
@@ -284,7 +284,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The [matrix-synapse](options.html#opt-services.matrix-synapse.enable) module no longer includes optional dependencies by default, they have to be added through the [plugins](options.html#opt-services.matrix-synapse.plugins) option.
 
-- `buildGoModule` now internally creates a vendor directory in the source tree for downloaded modules instead of using go\'s [module proxy protocol](https://golang.org/cmd/go/#hdr-Module_proxy_protocol). This storage format is simpler and therefore less likely to break with future versions of go. As a result `buildGoModule` switched from `modSha256` to the `vendorSha256` attribute to pin fetched version data.
+- `buildGoModule` now internally creates a vendor directory in the source tree for downloaded modules instead of using go's [module proxy protocol](https://golang.org/cmd/go/#hdr-Module_proxy_protocol). This storage format is simpler and therefore less likely to break with future versions of go. As a result `buildGoModule` switched from `modSha256` to the `vendorSha256` attribute to pin fetched version data.
 
 - Grafana is now built without support for phantomjs by default. Phantomjs support has been [deprecated in Grafana](https://grafana.com/docs/grafana/latest/guides/whats-new-in-v6-4/) and the phantomjs project is [currently unmaintained](https://github.com/ariya/phantomjs/issues/15344#issue-302015362). It can still be enabled by providing `phantomJsSupport = true` to the package instantiation:
 
@@ -306,9 +306,9 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The initrd SSH support now uses OpenSSH rather than Dropbear to allow the use of Ed25519 keys and other OpenSSH-specific functionality. Host keys must now be in the OpenSSH format, and at least one pre-generated key must be specified.
 
-  If you used the `boot.initrd.network.ssh.host*Key` options, you\'ll get an error explaining how to convert your host keys and migrate to the new `boot.initrd.network.ssh.hostKeys` option. Otherwise, if you don\'t have any host keys set, you\'ll need to generate some; see the `hostKeys` option documentation for instructions.
+  If you used the `boot.initrd.network.ssh.host*Key` options, you'll get an error explaining how to convert your host keys and migrate to the new `boot.initrd.network.ssh.hostKeys` option. Otherwise, if you don't have any host keys set, you'll need to generate some; see the `hostKeys` option documentation for instructions.
 
-- Since this release there\'s an easy way to customize your PHP install to get a much smaller base PHP with only wanted extensions enabled. See the following snippet installing a smaller PHP with the extensions `imagick`, `opcache`, `pdo` and `pdo_mysql` loaded:
+- Since this release there's an easy way to customize your PHP install to get a much smaller base PHP with only wanted extensions enabled. See the following snippet installing a smaller PHP with the extensions `imagick`, `opcache`, `pdo` and `pdo_mysql` loaded:
 
   ```nix
   {
@@ -325,7 +325,7 @@ When upgrading from a previous release, please be aware of the following incompa
   }
   ```
 
-  The default `php` attribute hasn\'t lost any extensions. The `opcache` extension has been added. All upstream PHP extensions are available under php.extensions.\<name?\>.
+  The default `php` attribute hasn't lost any extensions. The `opcache` extension has been added. All upstream PHP extensions are available under php.extensions.\<name?\>.
 
   All PHP `config` flags have been removed for the following reasons:
 
@@ -418,9 +418,9 @@ When upgrading from a previous release, please be aware of the following incompa
 
   The default value for [services.httpd.mpm](options.html#opt-services.httpd.mpm) has been changed from `prefork` to `event`. Along with this change the default value for [services.httpd.virtualHosts.\<name\>.http2](options.html#opt-services.httpd.virtualHosts) has been set to `true`.
 
-- The `systemd-networkd` option `systemd.network.networks.<name>.dhcp.CriticalConnection` has been removed following upstream systemd\'s deprecation of the same. It is recommended to use `systemd.network.networks.<name>.networkConfig.KeepConfiguration` instead. See systemd.network 5 for details.
+- The `systemd-networkd` option `systemd.network.networks.<name>.dhcp.CriticalConnection` has been removed following upstream systemd's deprecation of the same. It is recommended to use `systemd.network.networks.<name>.networkConfig.KeepConfiguration` instead. See systemd.network 5 for details.
 
-- The `systemd-networkd` option `systemd.network.networks._name_.dhcpConfig` has been renamed to [systemd.network.networks._name_.dhcpV4Config](options.html#opt-systemd.network.networks._name_.dhcpV4Config) following upstream systemd\'s documentation change. See systemd.network 5 for details.
+- The `systemd-networkd` option `systemd.network.networks._name_.dhcpConfig` has been renamed to [systemd.network.networks._name_.dhcpV4Config](options.html#opt-systemd.network.networks._name_.dhcpV4Config) following upstream systemd's documentation change. See systemd.network 5 for details.
 
 - In the `picom` module, several options that accepted floating point numbers encoded as strings (for example [services.picom.activeOpacity](options.html#opt-services.picom.activeOpacity)) have been changed to the (relatively) new native `float` type. To migrate your configuration simply remove the quotes around the numbers.
 
@@ -440,7 +440,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The GRUB specific option `boot.loader.grub.extraInitrd` has been replaced with the generic option `boot.initrd.secrets`. This option creates a secondary initrd from the specified files, rather than using a manually created initrd file. Due to an existing bug with `boot.loader.grub.extraInitrd`, it is not possible to directly boot an older generation that used that option. It is still possible to rollback to that generation if the required initrd file has not been deleted.
 
-- The [DNSChain](https://github.com/okTurtles/dnschain) package and NixOS module have been removed from Nixpkgs as the software is unmaintained and can\'t be built. For more information see issue [\#89205](https://github.com/NixOS/nixpkgs/issues/89205).
+- The [DNSChain](https://github.com/okTurtles/dnschain) package and NixOS module have been removed from Nixpkgs as the software is unmaintained and can't be built. For more information see issue [\#89205](https://github.com/NixOS/nixpkgs/issues/89205).
 
 - In the `resilio` module, [services.resilio.httpListenAddr](options.html#opt-services.resilio.httpListenAddr) has been changed to listen to `[::1]` instead of `0.0.0.0`.
 
@@ -456,7 +456,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
   - Update servers first, then clients.
 
-- Radicale\'s default package has changed from 2.x to 3.x. An upgrade checklist can be found [here](https://github.com/Kozea/Radicale/blob/3.0.x/NEWS.md#upgrade-checklist). You can use the newer version in the NixOS service by setting the `package` to `radicale3`, which is done automatically if `stateVersion` is 20.09 or higher.
+- Radicale's default package has changed from 2.x to 3.x. An upgrade checklist can be found [here](https://github.com/Kozea/Radicale/blob/3.0.x/NEWS.md#upgrade-checklist). You can use the newer version in the NixOS service by setting the `package` to `radicale3`, which is done automatically if `stateVersion` is 20.09 or higher.
 
 - `udpt` experienced a complete rewrite from C++ to rust. The configuration format changed from ini to toml. The new configuration documentation can be found at [the official website](https://naim94a.github.io/udpt/config.html) and example configuration is packaged in `${udpt}/share/udpt/udpt.toml`.
 
@@ -522,7 +522,7 @@ When upgrading from a previous release, please be aware of the following incompa
   }
   ```
 
-  The base package has also been upgraded to the 2020-07-29 \"Hogfather\" release. Plugins might be incompatible or require upgrading.
+  The base package has also been upgraded to the 2020-07-29 "Hogfather" release. Plugins might be incompatible or require upgrading.
 
 - The [services.postgresql.dataDir](options.html#opt-services.postgresql.dataDir) option is now set to `"/var/lib/postgresql/${cfg.package.psqlSchema}"` regardless of your [system.stateVersion](options.html#opt-system.stateVersion). Users with an existing postgresql install that have a [system.stateVersion](options.html#opt-system.stateVersion) of `17.03` or below should double check what the value of their [services.postgresql.dataDir](options.html#opt-services.postgresql.dataDir) option is (`/var/db/postgresql`) and then explicitly set this value to maintain compatibility:
 
@@ -552,17 +552,17 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The [jellyfin](options.html#opt-services.jellyfin.enable) module will use and stay on the Jellyfin version `10.5.5` if `stateVersion` is lower than `20.09`. This is because significant changes were made to the database schema, and it is highly recommended to backup your instance before upgrading. After making your backup, you can upgrade to the latest version either by setting your `stateVersion` to `20.09` or higher, or set the `services.jellyfin.package` to `pkgs.jellyfin`. If you do not wish to upgrade Jellyfin, but want to change your `stateVersion`, you can set the value of `services.jellyfin.package` to `pkgs.jellyfin_10_5`.
 
-- The `security.rngd` service is now disabled by default. This choice was made because there\'s krngd in the linux kernel space making it (for most usecases) functionally redundent.
+- The `security.rngd` service is now disabled by default. This choice was made because there's krngd in the linux kernel space making it (for most usecases) functionally redundent.
 
 - The `hardware.nvidia.optimus_prime.enable` service has been renamed to `hardware.nvidia.prime.sync.enable` and has many new enhancements. Related nvidia prime settings may have also changed.
 
 - The package nextcloud17 has been removed and nextcloud18 was marked as insecure since both of them will [ will be EOL (end of life) within the lifetime of 20.09](https://docs.nextcloud.com/server/19/admin_manual/release_schedule.html).
 
-  It\'s necessary to upgrade to nextcloud19:
+  It's necessary to upgrade to nextcloud19:
 
-  - From nextcloud17, you have to upgrade to nextcloud18 first as Nextcloud doesn\'t allow going multiple major revisions forward in a single upgrade. This is possible by setting [services.nextcloud.package](options.html#opt-services.nextcloud.package) to nextcloud18.
+  - From nextcloud17, you have to upgrade to nextcloud18 first as Nextcloud doesn't allow going multiple major revisions forward in a single upgrade. This is possible by setting [services.nextcloud.package](options.html#opt-services.nextcloud.package) to nextcloud18.
 
-  - From nextcloud18, it\'s possible to directly upgrade to nextcloud19 by setting [services.nextcloud.package](options.html#opt-services.nextcloud.package) to nextcloud19.
+  - From nextcloud18, it's possible to directly upgrade to nextcloud19 by setting [services.nextcloud.package](options.html#opt-services.nextcloud.package) to nextcloud19.
 
 - The GNOME desktop manager no longer default installs gnome3.epiphany. It was chosen to do this as it has a usability breaking issue (see issue [\#98819](https://github.com/NixOS/nixpkgs/issues/98819)) that makes it unsuitable to be a default app.
 
@@ -578,7 +578,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - `services.journald.rateLimitBurst` was updated from `1000` to `10000` to follow the new upstream systemd default.
 
-- The notmuch package moves its emacs-related binaries and emacs lisp files to a separate output. They\'re not part of the default `out` output anymore - if you relied on the `notmuch-emacs-mua` binary or the emacs lisp files, access them via the `notmuch.emacs` output.
+- The notmuch package moves its emacs-related binaries and emacs lisp files to a separate output. They're not part of the default `out` output anymore - if you relied on the `notmuch-emacs-mua` binary or the emacs lisp files, access them via the `notmuch.emacs` output.
 
 - Device tree overlay support was improved in [\#79370](https://github.com/NixOS/nixpkgs/pull/79370) and now uses [hardware.deviceTree.kernelPackage](options.html#opt-hardware.deviceTree.kernelPackage) instead of `hardware.deviceTree.base`. [hardware.deviceTree.overlays](options.html#opt-hardware.deviceTree.overlays) configuration was extended to support `.dts` files with symbols. Device trees can now be filtered by setting [hardware.deviceTree.filter](options.html#opt-hardware.deviceTree.filter) option.
 
@@ -590,7 +590,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
   Please note that Rust packages utilizing a custom build/install procedure (e.g. by using a `Makefile`) or test suites that rely on the structure of the `target/` directory may break due to those assumptions. For further information, please read the Rust section in the Nixpkgs manual.
 
-- The cc- and binutils-wrapper\'s \"infix salt\" and `_BUILD_` and `_TARGET_` user infixes have been replaced with with a \"suffix salt\" and suffixes and `_FOR_BUILD` and `_FOR_TARGET`. This matches the autotools convention for env vars which standard for these things, making interfacing with other tools easier.
+- The cc- and binutils-wrapper's "infix salt" and `_BUILD_` and `_TARGET_` user infixes have been replaced with with a "suffix salt" and suffixes and `_FOR_BUILD` and `_FOR_TARGET`. This matches the autotools convention for env vars which standard for these things, making interfacing with other tools easier.
 
 - Additional Git documentation (HTML and text files) is now available via the `git-doc` package.
 
@@ -598,7 +598,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The installer now enables sshd by default. This improves installation on headless machines especially ARM single-board-computer. To login through ssh, either a password or an ssh key must be set for the root user or the nixos user.
 
-- The scripted networking system now uses `.link` files in `/etc/systemd/network` to configure mac address and link MTU, instead of the sometimes buggy `network-link-*` units, which have been removed. Bringing the interface up has been moved to the beginning of the `network-addresses-*` unit. Note this doesn\'t require `systemd-networkd` - it\'s udev that parses `.link` files. Extra care needs to be taken in the presence of [legacy udev rules](https://wiki.debian.org/NetworkInterfaceNames#THE_.22PERSISTENT_NAMES.22_SCHEME) to rename interfaces, as MAC Address and MTU defined in these options can only match on the original link name. In such cases, you most likely want to create a `10-*.link` file through [systemd.network.links](options.html#opt-systemd.network.links) and set both name and MAC Address / MTU there.
+- The scripted networking system now uses `.link` files in `/etc/systemd/network` to configure mac address and link MTU, instead of the sometimes buggy `network-link-*` units, which have been removed. Bringing the interface up has been moved to the beginning of the `network-addresses-*` unit. Note this doesn't require `systemd-networkd` - it's udev that parses `.link` files. Extra care needs to be taken in the presence of [legacy udev rules](https://wiki.debian.org/NetworkInterfaceNames#THE_.22PERSISTENT_NAMES.22_SCHEME) to rename interfaces, as MAC Address and MTU defined in these options can only match on the original link name. In such cases, you most likely want to create a `10-*.link` file through [systemd.network.links](options.html#opt-systemd.network.links) and set both name and MAC Address / MTU there.
 
 - Grafana received a major update to version 7.x. A plugin is now needed for image rendering support, and plugins must now be signed by default. More information can be found [in the Grafana documentation](https://grafana.com/docs/grafana/latest/installation/upgrading/#upgrading-to-v7-0).
 
@@ -624,15 +624,15 @@ When upgrading from a previous release, please be aware of the following incompa
 
   to get the previous behavior of listening on all network interfaces.
 
-- With this release `systemd-networkd` (when enabled through [networking.useNetworkd](options.html#opt-networking.useNetworkd)) has it\'s netlink socket created through a `systemd.socket` unit. This gives us control over socket buffer sizes and other parameters. For larger setups where networkd has to create a lot of (virtual) devices the default buffer size (currently 128MB) is not enough.
+- With this release `systemd-networkd` (when enabled through [networking.useNetworkd](options.html#opt-networking.useNetworkd)) has it's netlink socket created through a `systemd.socket` unit. This gives us control over socket buffer sizes and other parameters. For larger setups where networkd has to create a lot of (virtual) devices the default buffer size (currently 128MB) is not enough.
 
   On a machine with \>100 virtual interfaces (e.g., wireguard tunnels, VLANs, ...), that all have to be brought up during system startup, the receive buffer size will spike for a brief period. Eventually some of the message will be dropped since there is not enough (permitted) buffer space available.
 
   By having `systemd-networkd` start with a netlink socket created by `systemd` we can configure the `ReceiveBufferSize=` parameter in the socket options (i.e. `systemd.sockets.systemd-networkd.socketOptions.ReceiveBufferSize`) without recompiling `systemd-networkd`.
 
-  Since the actual memory requirements depend on hardware, timing, exact configurations etc. it isn\'t currently possible to infer a good default from within the NixOS module system. Administrators are advised to monitor the logs of `systemd-networkd` for `rtnl: kernel receive buffer overrun` spam and increase the memory limit as they see fit.
+  Since the actual memory requirements depend on hardware, timing, exact configurations etc. it isn't currently possible to infer a good default from within the NixOS module system. Administrators are advised to monitor the logs of `systemd-networkd` for `rtnl: kernel receive buffer overrun` spam and increase the memory limit as they see fit.
 
-  Note: Increasing the `ReceiveBufferSize=` doesn\'t allocate any memory. It just increases the upper bound on the kernel side. The memory allocation depends on the amount of messages that are queued on the kernel side of the netlink socket.
+  Note: Increasing the `ReceiveBufferSize=` doesn't allocate any memory. It just increases the upper bound on the kernel side. The memory allocation depends on the amount of messages that are queued on the kernel side of the netlink socket.
 
 - Specifying [mailboxes](options.html#opt-services.dovecot2.mailboxes) in the dovecot2 module as a list is deprecated and will break eval in 21.05. Instead, an attribute-set should be specified where the `name` should be the key of the attribute.
 
@@ -662,7 +662,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - nextcloud has been updated to [v19](https://nextcloud.com/blog/nextcloud-hub-brings-productivity-to-home-office/).
 
-  If you have an existing installation, please make sure that you\'re on nextcloud18 before upgrading to nextcloud19 since Nextcloud doesn\'t support upgrades across multiple major versions.
+  If you have an existing installation, please make sure that you're on nextcloud18 before upgrading to nextcloud19 since Nextcloud doesn't support upgrades across multiple major versions.
 
 - The `nixos-run-vms` script now deletes the previous run machines states on test startup. You can use the `--keep-vm-state` flag to match the previous behaviour and keep the same VM state between different test runs.
 
diff --git a/nixos/doc/manual/release-notes/rl-2105.section.md b/nixos/doc/manual/release-notes/rl-2105.section.md
index 77c4a9cd7a0..6244d79e7e7 100644
--- a/nixos/doc/manual/release-notes/rl-2105.section.md
+++ b/nixos/doc/manual/release-notes/rl-2105.section.md
@@ -68,9 +68,9 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - If the `services.dbus` module is enabled, then the user D-Bus session is now always socket activated. The associated options `services.dbus.socketActivated` and `services.xserver.startDbusSession` have therefore been removed and you will receive a warning if they are present in your configuration. This change makes the user D-Bus session available also for non-graphical logins.
 
-- The `networking.wireless.iwd` module now installs the upstream-provided 80-iwd.link file, which sets the NamePolicy= for all wlan devices to \"keep kernel\", to avoid race conditions between iwd and networkd. If you don\'t want this, you can set `systemd.network.links."80-iwd" = lib.mkForce {}`.
+- The `networking.wireless.iwd` module now installs the upstream-provided 80-iwd.link file, which sets the NamePolicy= for all wlan devices to "keep kernel", to avoid race conditions between iwd and networkd. If you don't want this, you can set `systemd.network.links."80-iwd" = lib.mkForce {}`.
 
-- `rubyMinimal` was removed due to being unused and unusable. The default ruby interpreter includes JIT support, which makes it reference it\'s compiler. Since JIT support is probably needed by some Gems, it was decided to enable this feature with all cc references by default, and allow to build a Ruby derivation without references to cc, by setting `jitSupport = false;` in an overlay. See [\#90151](https://github.com/NixOS/nixpkgs/pull/90151) for more info.
+- `rubyMinimal` was removed due to being unused and unusable. The default ruby interpreter includes JIT support, which makes it reference it's compiler. Since JIT support is probably needed by some Gems, it was decided to enable this feature with all cc references by default, and allow to build a Ruby derivation without references to cc, by setting `jitSupport = false;` in an overlay. See [\#90151](https://github.com/NixOS/nixpkgs/pull/90151) for more info.
 
 - Setting `services.openssh.authorizedKeysFiles` now also affects which keys `security.pam.enableSSHAgentAuth` will use. WARNING: If you are using these options in combination do make sure that any key paths you use are present in `services.openssh.authorizedKeysFiles`!
 
@@ -130,7 +130,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - `vim` and `neovim` switched to Python 3, dropping all Python 2 support.
 
-- [networking.wireguard.interfaces.\<name\>.generatePrivateKeyFile](options.html#opt-networking.wireguard.interfaces), which is off by default, had a `chmod` race condition fixed. As an aside, the parent directory\'s permissions were widened, and the key files were made owner-writable. This only affects newly created keys. However, if the exact permissions are important for your setup, read [\#121294](https://github.com/NixOS/nixpkgs/pull/121294).
+- [networking.wireguard.interfaces.\<name\>.generatePrivateKeyFile](options.html#opt-networking.wireguard.interfaces), which is off by default, had a `chmod` race condition fixed. As an aside, the parent directory's permissions were widened, and the key files were made owner-writable. This only affects newly created keys. However, if the exact permissions are important for your setup, read [\#121294](https://github.com/NixOS/nixpkgs/pull/121294).
 
 - [boot.zfs.forceImportAll](options.html#opt-boot.zfs.forceImportAll) previously did nothing, but has been fixed. However its default has been changed to `false` to preserve the existing default behaviour. If you have this explicitly set to `true`, please note that your non-root pools will now be forcibly imported.
 
@@ -157,12 +157,12 @@ When upgrading from a previous release, please be aware of the following incompa
 - Amazon EC2 and OpenStack Compute (nova) images now re-fetch instance meta data and user data from the instance metadata service (IMDS) on each boot. For example: stopping an EC2 instance, changing its user data, and restarting the instance will now cause it to fetch and apply the new user data.
 
   ::: {.warning}
-  Specifically, `/etc/ec2-metadata` is re-populated on each boot. Some NixOS scripts that read from this directory are guarded to only run if the files they want to manipulate do not already exist, and so will not re-apply their changes if the IMDS response changes. Examples: `root`\'s SSH key is only added if `/root/.ssh/authorized_keys` does not exist, and SSH host keys are only set from user data if they do not exist in `/etc/ssh`.
+  Specifically, `/etc/ec2-metadata` is re-populated on each boot. Some NixOS scripts that read from this directory are guarded to only run if the files they want to manipulate do not already exist, and so will not re-apply their changes if the IMDS response changes. Examples: `root`'s SSH key is only added if `/root/.ssh/authorized_keys` does not exist, and SSH host keys are only set from user data if they do not exist in `/etc/ssh`.
   :::
 
 - The `rspamd` services is now sandboxed. It is run as a dynamic user instead of root, so secrets and other files may have to be moved or their permissions may have to be fixed. The sockets are now located in `/run/rspamd` instead of `/run`.
 
-- Enabling the Tor client no longer silently also enables and configures Privoxy, and the `services.tor.client.privoxy.enable` option has been removed. To enable Privoxy, and to configure it to use Tor\'s faster port, use the following configuration:
+- Enabling the Tor client no longer silently also enables and configures Privoxy, and the `services.tor.client.privoxy.enable` option has been removed. To enable Privoxy, and to configure it to use Tor's faster port, use the following configuration:
 
   ```nix
   {
@@ -181,7 +181,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - The fish-foreign-env package has been replaced with fishPlugins.foreign-env, in which the fish functions have been relocated to the `vendor_functions.d` directory to be loaded automatically.
 
-- The prometheus json exporter is now managed by the prometheus community. Together with additional features some backwards incompatibilities were introduced. Most importantly the exporter no longer accepts a fixed command-line parameter to specify the URL of the endpoint serving JSON. It now expects this URL to be passed as an URL parameter, when scraping the exporter\'s `/probe` endpoint. In the prometheus scrape configuration the scrape target might look like this:
+- The prometheus json exporter is now managed by the prometheus community. Together with additional features some backwards incompatibilities were introduced. Most importantly the exporter no longer accepts a fixed command-line parameter to specify the URL of the endpoint serving JSON. It now expects this URL to be passed as an URL parameter, when scraping the exporter's `/probe` endpoint. In the prometheus scrape configuration the scrape target might look like this:
 
   ```
   http://some.json-exporter.host:7979/probe?target=https://example.com/some/json/endpoint
@@ -230,7 +230,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
   Additionally, packages flashplayer and hal-flash were removed along with the `services.flashpolicyd` module.
 
-- The `security.rngd` module has been removed. It was disabled by default in 20.09 as it was functionally redundant with krngd in the linux kernel. It is not necessary for any device that the kernel recognises as an hardware RNG, as it will automatically run the krngd task to periodically collect random data from the device and mix it into the kernel\'s RNG.
+- The `security.rngd` module has been removed. It was disabled by default in 20.09 as it was functionally redundant with krngd in the linux kernel. It is not necessary for any device that the kernel recognises as an hardware RNG, as it will automatically run the krngd task to periodically collect random data from the device and mix it into the kernel's RNG.
 
   The default SMTP port for GitLab has been changed to `25` from its previous default of `465`. If you depended on this default, you should now set the [services.gitlab.smtp.port](options.html#opt-services.gitlab.smtp.port) option.
 
@@ -272,11 +272,11 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - `environment.defaultPackages` now includes the nano package. If pkgs.nano is not added to the list, make sure another editor is installed and the `EDITOR` environment variable is set to it. Environment variables can be set using `environment.variables`.
 
-- `services.minio.dataDir` changed type to a list of paths, required for specifiyng multiple data directories for using with erasure coding. Currently, the service doesn\'t enforce nor checks the correct number of paths to correspond to minio requirements.
+- `services.minio.dataDir` changed type to a list of paths, required for specifiyng multiple data directories for using with erasure coding. Currently, the service doesn't enforce nor checks the correct number of paths to correspond to minio requirements.
 
 - All CUDA toolkit versions prior to CUDA 10 have been removed.
 
-- The kbdKeymaps package was removed since dvp and neo are now included in kbd. If you want to use the Programmer Dvorak Keyboard Layout, you have to use `dvorak-programmer` in `console.keyMap` now instead of `dvp`. In `services.xserver.xkbVariant` it\'s still `dvp`.
+- The kbdKeymaps package was removed since dvp and neo are now included in kbd. If you want to use the Programmer Dvorak Keyboard Layout, you have to use `dvorak-programmer` in `console.keyMap` now instead of `dvp`. In `services.xserver.xkbVariant` it's still `dvp`.
 
 - The babeld service is now being run as an unprivileged user. To achieve that the module configures `skip-kernel-setup true` and takes care of setting forwarding and rp_filter sysctls by itself as well as for each interface in `services.babeld.interfaces`.
 
@@ -286,7 +286,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - Instead of determining `services.radicale.package` automatically based on `system.stateVersion`, the latest version is always used because old versions are not officially supported.
 
-  Furthermore, Radicale\'s systemd unit was hardened which might break some deployments. In particular, a non-default `filesystem_folder` has to be added to `systemd.services.radicale.serviceConfig.ReadWritePaths` if the deprecated `services.radicale.config` is used.
+  Furthermore, Radicale's systemd unit was hardened which might break some deployments. In particular, a non-default `filesystem_folder` has to be added to `systemd.services.radicale.serviceConfig.ReadWritePaths` if the deprecated `services.radicale.config` is used.
 
 - In the `security.acme` module, use of `--reuse-key` parameter for Lego has been removed. It was introduced for HKPK, but this security feature is now deprecated. It is a better security practice to rotate key pairs instead of always keeping the same. If you need to keep this parameter, you can add it back using `extraLegoRenewFlags` as an option for the appropriate certificate.
 
@@ -294,13 +294,13 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - `stdenv.lib` has been deprecated and will break eval in 21.11. Please use `pkgs.lib` instead. See [\#108938](https://github.com/NixOS/nixpkgs/issues/108938) for details.
 
-- [GNURadio](https://www.gnuradio.org/) has a `pkgs` attribute set, and there\'s a `gnuradio.callPackage` function that extends `pkgs` with a `mkDerivation`, and a `mkDerivationWith`, like Qt5. Now all `gnuradio.pkgs` are defined with `gnuradio.callPackage` and some packages that depend on gnuradio are defined with this as well.
+- [GNURadio](https://www.gnuradio.org/) has a `pkgs` attribute set, and there's a `gnuradio.callPackage` function that extends `pkgs` with a `mkDerivation`, and a `mkDerivationWith`, like Qt5. Now all `gnuradio.pkgs` are defined with `gnuradio.callPackage` and some packages that depend on gnuradio are defined with this as well.
 
 - [Privoxy](https://www.privoxy.org/) has been updated to version 3.0.32 (See [announcement](https://lists.privoxy.org/pipermail/privoxy-announce/2021-February/000007.html)). Compared to the previous release, Privoxy has gained support for HTTPS inspection (still experimental), Brotli decompression, several new filters and lots of bug fixes, including security ones. In addition, the package is now built with compression and external filters support, which were previously disabled.
 
   Regarding the NixOS module, new options for HTTPS inspection have been added and `services.privoxy.extraConfig` has been replaced by the new [services.privoxy.settings](options.html#opt-services.privoxy.settings) (See [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) for the motivation).
 
-- [Kodi](https://kodi.tv/) has been updated to version 19.1 \"Matrix\". See the [announcement](https://kodi.tv/article/kodi-19-0-matrix-release) for further details.
+- [Kodi](https://kodi.tv/) has been updated to version 19.1 "Matrix". See the [announcement](https://kodi.tv/article/kodi-19-0-matrix-release) for further details.
 
 - The `services.packagekit.backend` option has been removed as it only supported a single setting which would always be the default. Instead new [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md) compliant [services.packagekit.settings](options.html#opt-services.packagekit.settings) and [services.packagekit.vendorSettings](options.html#opt-services.packagekit.vendorSettings) options have been introduced.
 
@@ -316,13 +316,13 @@ When upgrading from a previous release, please be aware of the following incompa
 
   If this option is disabled, default MTA config becomes not set and you should set the options in `services.mailman.settings.mta` according to the desired configuration as described in [Mailman documentation](https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html).
 
-- The default-version of `nextcloud` is nextcloud21. Please note that it\'s _not_ possible to upgrade `nextcloud` across multiple major versions! This means that it\'s e.g. not possible to upgrade from nextcloud18 to nextcloud20 in a single deploy and most `20.09` users will have to upgrade to nextcloud20 first.
+- The default-version of `nextcloud` is nextcloud21. Please note that it's _not_ possible to upgrade `nextcloud` across multiple major versions! This means that it's e.g. not possible to upgrade from nextcloud18 to nextcloud20 in a single deploy and most `20.09` users will have to upgrade to nextcloud20 first.
 
   The package can be manually upgraded by setting [services.nextcloud.package](options.html#opt-services.nextcloud.package) to nextcloud21.
 
 - The setting [services.redis.bind](options.html#opt-services.redis.bind) defaults to `127.0.0.1` now, making Redis listen on the loopback interface only, and not all public network interfaces.
 
-- NixOS now emits a deprecation warning if systemd\'s `StartLimitInterval` setting is used in a `serviceConfig` section instead of in a `unitConfig`; that setting is deprecated and now undocumented for the service section by systemd upstream, but still effective and somewhat buggy there, which can be confusing. See [\#45785](https://github.com/NixOS/nixpkgs/issues/45785) for details.
+- NixOS now emits a deprecation warning if systemd's `StartLimitInterval` setting is used in a `serviceConfig` section instead of in a `unitConfig`; that setting is deprecated and now undocumented for the service section by systemd upstream, but still effective and somewhat buggy there, which can be confusing. See [\#45785](https://github.com/NixOS/nixpkgs/issues/45785) for details.
 
   All services should use [systemd.services._name_.startLimitIntervalSec](options.html#opt-systemd.services._name_.startLimitIntervalSec) or `StartLimitIntervalSec` in [systemd.services._name_.unitConfig](options.html#opt-systemd.services._name_.unitConfig) instead.
 
@@ -357,7 +357,7 @@ When upgrading from a previous release, please be aware of the following incompa
 
   `services.unbound.forwardAddresses` and `services.unbound.allowedAccess` have also been changed to use the new settings interface. You can follow the instructions when executing `nixos-rebuild` to upgrade your configuration to use the new interface.
 
-- The `services.dnscrypt-proxy2` module now takes the upstream\'s example configuration and updates it with the user\'s settings. An option has been added to restore the old behaviour if you prefer to declare the configuration from scratch.
+- The `services.dnscrypt-proxy2` module now takes the upstream's example configuration and updates it with the user's settings. An option has been added to restore the old behaviour if you prefer to declare the configuration from scratch.
 
 - NixOS now defaults to the unified cgroup hierarchy (cgroupsv2). See the [Fedora Article for 31](https://www.redhat.com/sysadmin/fedora-31-control-group-v2) for details on why this is desirable, and how it impacts containers.
 
@@ -367,11 +367,11 @@ When upgrading from a previous release, please be aware of the following incompa
 
 - GNOME users may wish to delete their `~/.config/pulse` due to the changes to stream routing logic. See [PulseAudio bug 832](https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/832) for more information.
 
-- The zookeeper package does not provide `zooInspector.sh` anymore, as that \"contrib\" has been dropped from upstream releases.
+- The zookeeper package does not provide `zooInspector.sh` anymore, as that "contrib" has been dropped from upstream releases.
 
 - In the ACME module, the data used to build the hash for the account directory has changed to accommodate new features to reduce account rate limit issues. This will trigger new account creation on the first rebuild following this update. No issues are expected to arise from this, thanks to the new account creation handling.
 
-- [users.users._name_.createHome](options.html#opt-users.users._name_.createHome) now always ensures home directory permissions to be `0700`. Permissions had previously been ignored for already existing home directories, possibly leaving them readable by others. The option\'s description was incorrect regarding ownership management and has been simplified greatly.
+- [users.users._name_.createHome](options.html#opt-users.users._name_.createHome) now always ensures home directory permissions to be `0700`. Permissions had previously been ignored for already existing home directories, possibly leaving them readable by others. The option's description was incorrect regarding ownership management and has been simplified greatly.
 
 - When defining a new user, one of [users.users._name_.isNormalUser](options.html#opt-users.users._name_.isNormalUser) and [users.users._name_.isSystemUser](options.html#opt-users.users._name_.isSystemUser) is now required. This is to prevent accidentally giving a UID above 1000 to system users, which could have unexpected consequences, like running user activation scripts for system users. Note that users defined with an explicit UID below 500 are exempted from this check, as [users.users._name_.isSystemUser](options.html#opt-users.users._name_.isSystemUser) has no effect for those.
 
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
index fc4b44957c3..7272e923158 100644
--- a/nixos/doc/manual/release-notes/rl-2111.section.md
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -235,7 +235,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - The `erigon` ethereum node has moved to a new database format in `2021-05-04`, and requires a full resync
 
-- The `erigon` ethereum node has moved it's database location in `2021-08-03`, users upgrading must manually move their chaindata (see [release notes](https://github.com/ledgerwatch/erigon/releases/tag/v2021.08.03)).
+- The `erigon` ethereum node has moved its database location in `2021-08-03`, users upgrading must manually move their chaindata (see [release notes](https://github.com/ledgerwatch/erigon/releases/tag/v2021.08.03)).
 
 - [users.users.&lt;name&gt;.group](options.html#opt-users.users._name_.group) no longer defaults to `nogroup`, which was insecure. Out-of-tree modules are likely to require adaptation: instead of
   ```nix
diff --git a/nixos/doc/manual/release-notes/rl-2205.section.md b/nixos/doc/manual/release-notes/rl-2205.section.md
index 7cc0c308ee6..e73be3773c9 100644
--- a/nixos/doc/manual/release-notes/rl-2205.section.md
+++ b/nixos/doc/manual/release-notes/rl-2205.section.md
@@ -9,8 +9,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 - 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),  
+  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
@@ -30,7 +30,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - Systemd has been upgraded to the version 250.
 
-- Pulseaudio has been updated to version 15.0 and now optionally 
+- 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
@@ -50,7 +50,7 @@ In addition to numerous new and upgraded packages, this release has the followin
   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).
@@ -65,7 +65,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [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).
+- [BaGet](https://loic-sharma.github.io/BaGet/), a lightweight NuGet and symbol server. Available at services.baget.
 
 - [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).
 
@@ -147,7 +147,7 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - [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).
+- [mediamtx](https://github.com/aler9/mediamtx), ready-to-use RTSP / RTMP / HLS server and proxy that allows to read, publish and proxy video and audio streams. Available as [services.mediamtx](#opt-services.mediamtx.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).
 
@@ -743,11 +743,11 @@ In addition to numerous new and upgraded packages, this release has the followin
 - 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) 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). 
+  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).
 
@@ -762,14 +762,14 @@ In addition to numerous new and upgraded packages, this release has the followin
   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. 
+- 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/), 
+
+  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. 
+  > 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
@@ -980,7 +980,7 @@ In addition to numerous new and upgraded packages, this release has the followin
   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.
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index 27bd64e514f..7eafa6a9bef 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -8,33 +8,175 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
+- Core version changes:
+
+  - default linux: 5.15 -\> 6.1, all supported kernels available
+
+  - systemd has been updated to v253.1, see [the pull request](https://github.com/NixOS/nixpkgs/pull/216826) for more info.
+    It's recommended to use `nixos-rebuild boot` and `reboot`, rather than `nixos-rebuild switch` - since in some rare cases
+    the switch of a live system might fail.
+
+  - glibc: 2.35 -\> 2.37
+
 - Cinnamon has been updated to 5.6, see [the pull request](https://github.com/NixOS/nixpkgs/pull/201328#issue-1449910204) for what is changed.
 
+- GNOME has been upgraded to version 44. Please see the [release notes](https://release.gnome.org/44/) for details.
+
+- KDE Plasma has been updated to v5.27, see [the release notes](https://kde.org/announcements/plasma/5/5.27.0/) for what is changed.
+
+- `nixos-rebuild` now supports an extra `--specialisation` option that can be used to change specialisation for `switch` and `test` commands.
+
+- `libxcrypt`, the library providing the `crypt(3)` password hashing function, is now built without support for algorithms not flagged [`strong`](https://github.com/besser82/libxcrypt/blob/v4.4.33/lib/hashes.conf#L48). This affects the availability of password hashing algorithms used for system login (`login(1)`, `passwd(1)`), but also Apache2 Basic-Auth, Samba, OpenLDAP, Dovecot, and [many other packages](https://github.com/search?q=repo%3ANixOS%2Fnixpkgs%20libxcrypt&type=code).
+
+- `boot.bootspec.enable` (internal option) is now enabled by default because [RFC-0125](https://github.com/NixOS/rfcs/pull/125) was merged. This means you will have a bootspec document called `boot.json` generated for each system and specialisation in the top-level. This is useful to enable advanced boot usecases in NixOS such as SecureBoot.
+
 ## New Services {#sec-release-23.05-new-services}
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
+- [Akkoma](https://akkoma.social), an ActivityPub microblogging server. Available as [services.akkoma](options.html#opt-services.akkoma.enable).
+
 - [blesh](https://github.com/akinomyoga/ble.sh), a line editor written in pure bash. Available as [programs.bash.blesh](#opt-programs.bash.blesh.enable).
 
+- [webhook](https://github.com/adnanh/webhook), a lightweight webhook server. Available as [services.webhook](#opt-services.webhook.enable).
+
+- [cups-pdf-to-pdf](https://github.com/alexivkin/CUPS-PDF-to-PDF), a pdf-generating cups backend based on [cups-pdf](https://www.cups-pdf.de/). Available as [services.printing.cups-pdf](#opt-services.printing.cups-pdf.enable).
+
+- [clash-verge](https://github.com/zzzgydi/clash-verge), A Clash GUI based on tauri. Available as [programs.clash-verge](#opt-programs.clash-verge.enable).
+
+- [Cloudlog](https://www.magicbug.co.uk/cloudlog/), a web-based Amateur Radio logging application. Available as [services.cloudlog](#opt-services.cloudlog.enable).
+
+- [Deepin Desktop Environment](https://github.com/linuxdeepin/dde), an elegant, easy to use and reliable desktop environment. Available as [services.xserver.desktopManager.deepin](options.html#opt-services.xserver.desktopManager.deepin).
+
+- [system-repart](https://www.freedesktop.org/software/systemd/man/systemd-repart.service.html), grow and add partitions to a partition table. Available as [systemd.repart](options.html#opt-systemd.repart) and [boot.initrd.systemd.repart](options.html#opt-boot.initrd.systemd.repart)
+
 - [fzf](https://github.com/junegunn/fzf), a command line fuzzyfinder. Available as [programs.fzf](#opt-programs.fzf.fuzzyCompletion).
 
+- [readarr](https://github.com/Readarr/Readarr), Book Manager and Automation (Sonarr for Ebooks). Available as [services.readarr](options.html#opt-services.readarr.enable).
+
+- [gemstash](https://github.com/rubygems/gemstash), a RubyGems.org cache and private gem server. Available as [services.gemstash](#opt-services.gemstash.enable).
+
+- [gmediarender](https://github.com/hzeller/gmrender-resurrect), a simple, headless UPnP/DLNA renderer.  Available as [services.gmediarender](options.html#opt-services.gmediarender.enable).
+
+- [hyprland](https://github.com/hyprwm/hyprland), a dynamic tiling Wayland compositor that doesn't sacrifice on its looks. Available as [programs.hyprland](#opt-programs.hyprland.enable).
+
+- [minipro](https://gitlab.com/DavidGriffith/minipro/), an open source program for controlling the MiniPRO TL866xx series of chip programmers. Available as [programs.minipro](options.html#opt-programs.minipro.enable).
+
+- [stevenblack-blocklist](https://github.com/StevenBlack/hosts), A unified hosts file with base extensions for blocking unwanted websites. Available as [networking.stevenblack](options.html#opt-networking.stevenblack.enable).
+
+- [Budgie Desktop](https://github.com/BuddiesOfBudgie/budgie-desktop), a familiar, modern desktop environment. Availabe as [services.xserver.desktopManager.budgie](options.html#opt-services.xserver.desktopManager.budgie).
+
+- [imaginary](https://github.com/h2non/imaginary), a microservice for high-level image processing that Nextcloud can use to generate previews. Available as [services.imaginary](#opt-services.imaginary.enable).
+
+- [opensearch](https://opensearch.org), a search server alternative to Elasticsearch. Available as [services.opensearch](options.html#opt-services.opensearch.enable).
+
+- [kavita](https://kavitareader.com), a self-hosted digital library. Available as [services.kavita](options.html#opt-services.kavita.enable).
+
+- [monica](https://www.monicahq.com), an open source personal CRM. Available as [services.monica](options.html#opt-services.monica.enable).
+
+- [authelia](https://www.authelia.com/), is an open-source authentication and authorization server. Available under [services.authelia](options.html#opt-services.authelia.enable).
+
+- [goeland](https://github.com/slurdge/goeland), an alternative to rss2email written in golang with many filters. Available as [services.goeland](#opt-services.goeland.enable).
+
+- [alertmanager-irc-relay](https://github.com/google/alertmanager-irc-relay), a Prometheus Alertmanager IRC Relay. Available as [services.prometheus.alertmanagerIrcRelay](options.html#opt-services.prometheus.alertmanagerIrcRelay.enable).
+
+- [tts](https://github.com/coqui-ai/TTS), a battle-tested deep learning toolkit for Text-to-Speech. Mutiple servers may be configured below [services.tts.servers](#opt-services.tts.servers).
+
 - [atuin](https://github.com/ellie/atuin), a sync server for shell history. Available as [services.atuin](#opt-services.atuin.enable).
 
+- [esphome](https://esphome.io), a dashboard to configure ESP8266/ESP32 devices for use with Home Automation systems. Available as [services.esphome](#opt-services.esphome.enable).
+
+- [networkd-dispatcher](https://gitlab.com/craftyguy/networkd-dispatcher), a dispatcher service for systemd-networkd connection status changes. Available as [services.networkd-dispatcher](#opt-services.networkd-dispatcher.enable).
+
+- [gonic](https://github.com/sentriz/gonic), a Subsonic music streaming server. Available as [services.gonic](#opt-services.gonic.enable).
+
 - [mmsd](https://gitlab.com/kop316/mmsd), a lower level daemon that transmits and recieves MMSes. Available as [services.mmsd](#opt-services.mmsd.enable).
 
+- [QDMR](https://dm3mat.darc.de/qdmr/), a GUI application and command line tool for programming DMR radios [programs.qdmr](#opt-programs.qdmr.enable)
+
+- [keyd](https://github.com/rvaiya/keyd), a key remapping daemon for linux. Available as [services.keyd](#opt-services.keyd.enable).
+
+- [consul-template](https://github.com/hashicorp/consul-template/), a template rendering, notifier, and supervisor for HashiCorp Consul and Vault data. Available as [services.consul-template](#opt-services.consul-template.instances).
+
+- [vault-agent](https://developer.hashicorp.com/vault/docs/agent), a template rendering and API auth proxy for HashiCorp Vault, similar to `consul-template`. Available as [services.vault-agent](#opt-services.vault-agent.instances).
+
 - [v2rayA](https://v2raya.org), a Linux web GUI client of Project V which supports V2Ray, Xray, SS, SSR, Trojan and Pingtunnel. Available as [services.v2raya](options.html#opt-services.v2raya.enable).
 
+- [wstunnel](https://github.com/erebe/wstunnel), a proxy tunnelling arbitrary TCP or UDP traffic through a WebSocket connection. Instances may be configured via [services.wstunnel](options.html#opt-services.wstunnel.enable).
+
+- [ulogd](https://www.netfilter.org/projects/ulogd/index.html), a userspace logging daemon for netfilter/iptables related logging. Available as [services.ulogd](options.html#opt-services.ulogd.enable).
+
+- [PufferPanel](https://pufferpanel.com), game server management panel designed to be easy to use. Available as [services.pufferpanel](#opt-services.pufferpanel.enable).
+
+- [jellyseerr](https://github.com/Fallenbagel/jellyseerr), a web-based requests manager for Jellyfin, forked from Overseerr. Available as [services.jellyseerr](#opt-services.jellyseerr.enable).
+
+- [stargazer](https://sr.ht/~zethra/stargazer/), a fast and easy to use Gemini server. Available as [services.stargazer](#opt-services.stargazer.enable).
+
+- [photoprism](https://photoprism.app/), a AI-Powered Photos App for the Decentralized Web. Available as [services.photoprism](options.html#opt-services.photoprism.enable).
+
+- [peroxide](https://github.com/ljanyst/peroxide), a fork of the official [ProtonMail bridge](https://github.com/ProtonMail/proton-bridge) that aims to be similar to [Hydroxide](https://github.com/emersion/hydroxide). Available as [services.peroxide](#opt-services.peroxide.enable).
+
+- [autosuspend](https://github.com/languitar/autosuspend), a python daemon that suspends a system if certain conditions are met, or not met.
+
+- [sharing](https://github.com/parvardegr/sharing), a command-line tool to share directories and files from the CLI to iOS and Android devices without the need of an extra client app. Available as [programs.sharing](#opt-programs.sharing.enable).
+
+- [nimdow](https://github.com/avahe-kellenberger/nimdow), a window manager written in Nim, inspired by dwm.
+
+- [trurl](https://github.com/curl/trurl), a command line tool for URL parsing and manipulation.
+
+- [wgautomesh](https://git.deuxfleurs.fr/Deuxfleurs/wgautomesh), a simple utility to help connect wireguard nodes together in a full mesh topology. Available as [services.wgautomesh](options.html#opt-services.wgautomesh.enable).
+
+- [woodpecker-agents](https://woodpecker-ci.org/), a simple CI engine with great extensibility. Available as [services.woodpecker-agents](#opt-services.woodpecker-agents.agents._name_.enable).
+
+- [woodpecker-server](https://woodpecker-ci.org/), a simple CI engine with great extensibility. Available as [services.woodpecker-server](#opt-services.woodpecker-server.enable).
+
+- [lldap](https://github.com/lldap/lldap), a lightweight authentication server that provides an opinionated, simplified LDAP interface for authentication. Available as [services.lldap](#opt-services.lldap.enable).
+
+- [ReGreet](https://github.com/rharish101/ReGreet), a clean and customizable greeter for greetd. Available as [programs.regreet](#opt-programs.regreet.enable).
+
+- [v4l2-relayd](https://git.launchpad.net/v4l2-relayd), a streaming relay for v4l2loopback using gstreamer. Available as [services.v4l2-relayd](#opt-services.v4l2-relayd.instances._name_.enable).
+
+- [hardware.ipu6](#opt-hardware.ipu6.enable) adds support for ipu6 based webcams on intel tiger lake and alder lake.
+
+- [ivpn](https://www.ivpn.net/), a secure, private VPN with fast WireGuard connections. Available as [services.ivpn](#opt-services.ivpn.enable).
+
 ## Backward Incompatibilities {#sec-release-23.05-incompatibilities}
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
 - `carnix` and `cratesIO` has been removed due to being unmaintained, use alternatives such as [naersk](https://github.com/nix-community/naersk) and [crate2nix](https://github.com/kolloch/crate2nix) instead.
 
-- `borgbackup` module now has an option for inhibiting system sleep while backups are running, defaulting to off (not inhibiting sleep), available as [`services.borgbackup.jobs.<name>.inhibitsSleep`](#opt-services.borgbackup.jobs._name_.inhibitsSleep). 
+- `services.asusd` configuration now uses strings instead of structured configuration, as upstream switched to the [RON](https://github.com/ron-rs/ron) configuration format. Support for structured configuration may return when [RON](https://github.com/ron-rs/ron) generation is implemented in nixpkgs.
+
+- `checkInputs` have been renamed to `nativeCheckInputs`, because they behave the same as `nativeBuildInputs` when `doCheck` is set. `checkInputs` now denote a new type of dependencies, added to `buildInputs` when `doCheck` is set. As a rule of thumb, `nativeCheckInputs` are tools on `$PATH` used during the tests, and `checkInputs` are libraries which are linked to executables built as part of the tests. Similarly, `installCheckInputs` are renamed to `nativeInstallCheckInputs`, corresponding to `nativeBuildInputs`, and `installCheckInputs` are a new type of dependencies added to `buildInputs` when `doInstallCheck` is set. (Note that this change will not cause breakage to derivations with `strictDeps` unset, which are most packages except python, rust, ocaml and go packages).
+
+- `buildDunePackage` now defaults to `strictDeps = true` which means that any library should go into `buildInputs` or `checkInputs`. Any executable that is run on the building machine should go into `nativeBuildInputs` or `nativeCheckInputs` respectively. Example of executables are `ocaml`, `findlib` and `menhir`. PPXs are libraries which are built by dune and should therefore not go into `nativeBuildInputs`.
+
+- `borgbackup` module now has an option for inhibiting system sleep while backups are running, defaulting to off (not inhibiting sleep), available as [`services.borgbackup.jobs.<name>.inhibitsSleep`](#opt-services.borgbackup.jobs._name_.inhibitsSleep).
+
+- The `ssh` client tool now disables the `~C` escape sequence by default. This can be re-enabled by setting `EnableEscapeCommandline yes`
+
+- The `ssh` module does not read `/etc/ssh/ssh_known_hosts2` anymore since this location is [deprecated since 2001](https://marc.info/?l=openssh-unix-dev&m=100508718416162&w=2).
+
+- The openssh module does not read `~/.ssh/authorized_keys2` anymore since this location is [deprecated since 2001](https://marc.info/?l=openssh-unix-dev&m=100508718416162&w=2).
+
+- `podman` now uses the `netavark` network stack. Users will need to delete all of their local containers, images, volumes, etc, by running `podman system reset --force` once before upgrading their systems.
+
+- `git-bug` has been updated to at least version 0.8.0, which includes backwards incompatible changes. The `git-bug-migration` package can be used to upgrade existing repositories.
+
+- `nushell` has been updated to at least version 0.77.0, which includes potential breaking changes in aliases. The old aliases are now available as `old-alias` but it is recommended you migrate to the new format. See [Reworked aliases](https://www.nushell.sh/blog/2023-03-14-nushell_0_77.html#reworked-aliases-breaking-changes-kubouch).
+
+- `keepassx` and `keepassx2` have been removed, due to upstream [stopping development](https://www.keepassx.org/index.html%3Fp=636.html). Consider [KeePassXC](https://keepassxc.org) as a maintained alternative.
+
+- The [services.kubo.settings](#opt-services.kubo.settings) option is now no longer stateful. If you changed any of the options in [services.kubo.settings](#opt-services.kubo.settings) in the past and then removed them from your NixOS configuration again, those changes are still in your Kubo configuration file but will now be reset to the default. If you're unsure, you may want to make a backup of your configuration file (probably /var/lib/ipfs/config) and compare after the update.
+
+- The Kubo HTTP API will no longer listen on localhost and will instead only listen on a Unix domain socket by default. Read the [services.kubo.settings.Addresses.API](#opt-services.kubo.settings.Addresses.API) option description for more information.
 
 - The EC2 image module no longer fetches instance metadata in stage-1. This results in a significantly smaller initramfs, since network drivers no longer need to be included, and faster boots, since metadata fetching can happen in parallel with startup of other services.
   This breaks services which rely on metadata being present by the time stage-2 is entered. Anything which reads EC2 metadata from `/etc/ec2-metadata` should now have an `after` dependency on `fetch-ec2-metadata.service`
 
+- `minio` removed support for its legacy filesystem backend in [RELEASE.2022-10-29T06-21-33Z](https://github.com/minio/minio/releases/tag/RELEASE.2022-10-29T06-21-33Z). This means if your storage was created with the old format, minio will no longer start. Unfortunately minio doesn't provide a an automatic migration, they only provide [instructions how to manually convert the node](https://min.io/docs/minio/windows/operations/install-deploy-manage/migrate-fs-gateway.html). To facilitate this migration we keep around the last version that still supports the old filesystem backend as `minio_legacy_fs`. Use it via `services.minio.package = minio_legacy_fs;` to export your data before switching to the new version. See the corresponding [issue](https://github.com/NixOS/nixpkgs/issues/199318) for more details.
+
 - `services.sourcehut.dispatch` and the corresponding package (`sourcehut.dispatchsrht`) have been removed due to [upstream deprecation](https://sourcehut.org/blog/2022-08-01-dispatch-deprecation-plans/).
 
 - The [services.snapserver.openFirewall](#opt-services.snapserver.openFirewall) module option default value has been changed from `true` to `false`. You will need to explicitly set this option to `true`, or configure your firewall.
@@ -43,51 +185,385 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 - The [services.unifi-video.openFirewall](#opt-services.unifi-video.openFirewall) module option default value has been changed from `true` to `false`. You will need to explicitly set this option to `true`, or configure your firewall.
 
-- The Nginx module now validates the syntax of config files at build time. For more complex configurations (using `include` with out-of-store files notably) you may need to disable this check by setting [services.nginx.validateConfig](#opt-services.nginx.validateConfig) to `false`.
+- The option `i18n.inputMethod.fcitx5.enableRimeData` has been removed. Default RIME data is now included in `fcitx5-rime` by default, and can be customized using `fcitx5-rime.override { rimeDataPkgs = [ pkgs.rime-data, package2, ... ]; }`
+
+- The udev hwdb.bin file is now built with systemd-hwdb rather than the [deprecated "udevadm hwdb"](https://github.com/systemd/systemd/pull/25714). This may impact mappings where the same key is defined in multiple matching entries. The updated behavior will select the latest definition in case of conflict. In general, this should be a positive change, as the hwdb source files are designed with this ordering in mind. As an example, the mapping of the HP Dev One keyboard scan code for "mute mic" is corrected by this update. This change may impact users who have worked-around previously incorrect mappings.
+
+- Kime has been updated from 2.5.6 to 3.0.2 and the `i18n.inputMethod.kime.config` option has been removed. Users should use `daemonModules`, `iconColor`, and `extraConfig` options under `i18n.inputMethod.kime` instead.
+
+- `tut` has been updated from 1.0.34 to 2.0.0, and now uses the TOML format for the configuration file instead of INI. Additional information can be found [here](https://github.com/RasmusLindroth/tut/releases/tag/2.0.0).
+
+- `i3status-rust` has been updated from 0.22.0 to 0.30.5, and this brings many changes to its configuration format. Additional information can be found [here](https://github.com/greshake/i3status-rust/blob/v0.30.0/NEWS.md).
+
+- The `wordpress` derivation no longer contains any builtin plugins or themes. If you need them you have to add them back to prevent your site from breaking. You can find them in `wordpressPackages.{plugins,themes}`.
+
+- `llvmPackages_rocm.llvm` will not contain `clang` or `compiler-rt`. `llvmPackages_rocm.clang` will not contain `llvm`. `llvmPackages_rocm.clangNoCompilerRt` has been removed in favor of using `llvmPackages_rocm.clang-unwrapped`.
+
+- `services.xserver.desktopManager.plasma5.excludePackages` has been moved to `environment.plasma5.excludePackages`, for consistency with other Desktop Environments
 
 - The EC2 image module previously detected and automatically mounted ext3-formatted instance store devices and partitions in stage-1 (initramfs), storing `/tmp` on the first discovered device. This behaviour, which only catered to very specific use cases and could not be disabled, has been removed. Users relying on this should provide their own implementation, and probably use ext4 and perform the mount in stage-2.
 
+- `teleport` has been upgraded from major version 10 to major version 12. Please see upstream [upgrade instructions](https://goteleport.com/docs/setup/operations/upgrading/) and release notes for versions [11](https://goteleport.com/docs/changelog/#1100) and [12](https://goteleport.com/docs/changelog/#1201). Note that Teleport does not officially support upgrades across more than one major version at a time. If you're running Teleport server components, it is recommended to first upgrade to an intermediate 11.x version by setting `services.teleport.package = pkgs.teleport_11`. Afterwards, this option can be removed to upgrade to the default version (12).
+
 - The EC2 image module previously detected and activated swap-formatted instance store devices and partitions in stage-1 (initramfs). This behaviour has been removed. Users relying on this should provide their own implementation.
 
+- `fail2ban` has been updated to 1.0.2, which has a few breaking changes compared to 0.11.2 ([changelog for 1.0.1](https://github.com/fail2ban/fail2ban/blob/1.0.1/ChangeLog), [changelog for 1.0.2](https://github.com/fail2ban/fail2ban/blob/1.0.2/ChangeLog))
+
+- Calling `makeSetupHook` without passing a `name` argument is deprecated.
+
+- Top-level buildPlatform,hostPlatform,targetPlatform have been deprecated, use stdenv.X instead.
+
+- `lib.systems.examples.ghcjs` and consequently `pkgsCross.ghcjs` now use the target triplet `javascript-unknown-ghcjs` instead of `js-unknown-ghcjs`. This has been done to match an [upstream decision](https://gitlab.haskell.org/ghc/ghc/-/commit/6636b670233522f01d002c9b97827d00289dbf5c) to follow Cabal's platform naming more closely. Nixpkgs will also reject `js` as an architecture name.
+
+- `dokuwiki` has been updated from 2023-07-31a (Igor) to 2023-04-04 (Jack Jackrum), which has [completely removed](https://www.dokuwiki.org/changes#release_2023-04-04_jack_jackrum) the options to embed HTML and PHP for security reasons. The [htmlok plugin](https://www.dokuwiki.org/plugin:htmlok) can be used to regain this functionality.
+
+- The old unsupported version 6.x of the ELK-stack and Elastic beats have been removed. Use OpenSearch instead.
+
+- The `cosmoc` package has been removed. The upstream scripts in `cosmocc` should be used instead.
+
 - Qt 5.12 and 5.14 have been removed, as the corresponding branches have been EOL upstream for a long time. This affected under 10 packages in nixpkgs, largely unmaintained upstream as well, however, out-of-tree package expressions may need to be updated manually.
 
+- The [services.wordpress.sites.&lt;name&gt;.plugins](#opt-services.wordpress.sites._name_.plugins) and [services.wordpress.sites.&lt;name&gt;.themes](#opt-services.wordpress.sites._name_.themes) options have been converted from sets to attribute sets to allow for consumers to specify explicit install paths via attribute name.
+
+- [`services.nextcloud.database.createLocally`](#opt-services.nextcloud.database.createLocally) now uses socket authentication and is no longer compatible with password authentication.
+  - If you want the module to manage the database for you, unset [`services.nextcloud.config.dbpassFile`](#opt-services.nextcloud.config.dbpassFile) (and [`services.nextcloud.config.dbhost`](#opt-services.nextcloud.config.dbhost), if it's set).
+  - If your database is external, simply set [`services.nextcloud.database.createLocally`](#opt-services.nextcloud.database.createLocally) to `false`.
+  - If you want to use password authentication **and** create the database locally, you will have to use [`services.mysql`](#opt-services.mysql.enable) to set it up.
+
+- `protonmail-bridge` package has been updated to major version 3.
+
+- Nebula now runs as a system user and group created for each nebula network, using the `CAP_NET_ADMIN` ambient capability on launch rather than starting as root. Ensure that any files each Nebula instance needs to access are owned by the correct user and group, by default `nebula-${networkName}`.
+
+- The `i18n.inputMethod.fcitx` option has been replaced with `i18n.inputMethod.fcitx5` because fcitx 4 `pkgs.fcitx` has been removed.
+
 - In `mastodon` it is now necessary to specify location of file with `PostgreSQL` database password. In `services.mastodon.database.passwordFile` parameter default value `/var/lib/mastodon/secrets/db-password` has been changed to `null`.
 
+- The `--target-host` and `--build-host` options of `nixos-rebuild` no longer treat the `localhost` value specially – to build on/deploy to local machine, omit the relevant flag.
+
 - The `nix.readOnlyStore` option has been renamed to `boot.readOnlyNixStore` to clarify that it configures the NixOS boot process, not the Nix daemon.
 
+- Deprecated `xlibsWrapper` transitional package has been removed in favour of direct use of its constitutents: `xorg.libX11`, `freetype` and others.
+
+- The latest available version of Nextcloud is v26 (available as `pkgs.nextcloud26`) which uses PHP 8.2 as interpreter by default. The installation logic is as follows:
+  - If `system.stateVersion` is >=23.05, `pkgs.nextcloud26` will be installed by default.
+  - If `system.stateVersion` is >=22.11, `pkgs.nextcloud25` will be installed by default.
+  - Please note that an upgrade from v24 (or older) to v26 directly is not possible. Please upgrade to `nextcloud25` (or earlier) first. Nextcloud prohibits skipping major versions while upgrading. You can upgrade by declaring [`services.nextcloud.package = pkgs.nextcloud25;`](options.html#opt-services.nextcloud.package).
+  - It's recommended to use the latest version available (i.e. v26) and to specify that using `services.nextcloud.package`.
+
+- .NET 5.0 and .NET 3.1 were removed due to being end-of-life, use a newer, supported .NET version - https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core
+
+- The iputils package, which is installed by default, no longer provides the
+  `ninfod`, `rarpd` and `rdisc` tools. See
+  [upstream's release notes](https://github.com/iputils/iputils/releases/tag/20221126)
+  for more details and available replacements.
+
+- The ppp plugin `rp-pppoe.so` has been renamed to `pppoe.so` in ppp 2.4.9. Starting from ppp 2.5.0, there is no longer a alias for backwards compatiblity. Configurations that use this plugin must be updated accordingly from `plugin rp-pppoe.so` to `plugin pppoe.so`. See [upstream change](https://github.com/ppp-project/ppp/commit/610a7bd76eb1f99f22317541b35001b1e24877ed).
+
+- [services.xserver.videoDrivers](options.html#opt-services.xserver.videoDrivers) now defaults to the `modesetting` driver over device-specific ones. The `radeon`, `amdgpu` and `nouveau` drivers are still available, but effectively unmaintained and not recommended for use.
+
+- To enable the HTTP3 (QUIC) protocol for a nginx virtual host, set the `quic` attribute on it to true, e.g. `services.nginx.virtualHosts.<name>.quic = true;`.
+
+- In `services.fail2ban`, `bantime-increment.<name>` options now default to `null` (except `bantime-increment.enable`) and are used to set the corresponding option in `jail.local` only if not `null`. Also, enforce that `bantime-increment.formula` and `bantime-increment.multipliers` are not both specified.
+
+- The default Asterisk package was changed to v20 from v19. Asterisk versions 16 and 19 have been dropped due to being EOL. You may need to update /var/lib/asterisk to match the template files in `${asterisk-20}/var/lib/asterisk`.
+
+- conntrack helper autodetection has been removed from kernels 6.0 and up upstream, and an assertion was added to ensure things don't silently stop working. Migrate your configuration to assign helpers explicitly or use an older LTS kernel branch as a temporary workaround.
+
+- The `services.pipewire.config` options have been removed, as they have basically never worked correctly. All behavior defined by the default configuration can be overridden with drop-in files as necessary - see [below](#sec-release-23.05-migration-pipewire) for details.
+
+- The catch-all `hardware.video.hidpi.enable` option was removed. Users on high density displays may want to:
+
+  - Set `services.xserver.upscaleDefaultCursor` to upscale the default X11 cursor for higher resolutions
+  - Adjust settings under `fonts.fontconfig` according to preference
+  - Adjust `console.font` according to preference, though the kernel will generally choose a reasonably sized font
+
+- `services.pipewire.media-session` and the `pipewire-media-session` package have been removed, as they are no longer supported upstream. Users are encouraged to use `services.pipewire.wireplumber` instead.
+
+- The `baget` package and module was removed due to being unmaintained.
+
+- The `qlandkartegt` and `garmindev` packages were removed due to being unmaintained and insecure.
+
+- `go-ethereum` package has been updated to v1.11.5 and the `puppeth` command is no longer available as of v1.11.0.
+
+- The `pnpm` package has be updated to from version 7.29.1 to version 8.1.1 and Node.js 14 support has been discontinued (though, there are workarounds if Node.js 14 is still required)
+  - Migration instructions: ["Before updating pnpm to v8 in your CI, regenerate your pnpm-lock.yaml. To upgrade your lockfile, run pnpm install and commit the changes. Existing dependencies will not be updated; however, due to configuration changes in pnpm v8, some missing peer dependencies may be added to the lockfile and some packages may get deduplicated. You can commit the new lockfile even before upgrading Node.js in the CI, as pnpm v7 already supports the new lockfile format."](https://github.com/pnpm/pnpm/releases/tag/v8.0.0)
+
+- The `zplug` package changes its output path from `$out` to `$out/share/zplug`. Users should update their dependency on `${pkgs.zplug}/init.zsh` to `${pkgs.zplug}/share/zplug/init.zsh`.
+
 ## Other Notable Changes {#sec-release-23.05-notable-changes}
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
-- `vim_configurable` has been renamed to `vim-full` to avoid confusion: `vim-full`'s build-time features are configurable, but both `vim` and `vim-full` are *customizable* (in the sense of user configuration, like vimrc).
+- `vim_configurable` has been renamed to `vim-full` to avoid confusion: `vim-full`'s build-time features are configurable, but both `vim` and `vim-full` are _customizable_ (in the sense of user configuration, like vimrc).
+
+- Pantheon now defaults to Mutter 43 and GNOME settings daemon 43, all Pantheon packages are now tracking elementary OS 7 updates.
 
 - The module for the application firewall `opensnitch` got the ability to configure rules. Available as [services.opensnitch.rules](#opt-services.opensnitch.rules)
 
 - The module `usbmuxd` now has the ability to change the package used by the daemon. In case you're experiencing issues with `usbmuxd` you can try an alternative program like `usbmuxd2`. Available as [services.usbmuxd.package](#opt-services.usbmuxd.package)
 
+- A few openssh options have been moved from extraConfig to the new freeform option `settings` and renamed as follows:
+  - `services.openssh.forwardX11` to `services.openssh.settings.X11Forwarding`
+  - `services.openssh.kbdInteractiveAuthentication` -> `services.openssh.settings.KbdInteractiveAuthentication`
+  - `services.openssh.passwordAuthentication` to `services.openssh.settings.PasswordAuthentication`
+  - `services.openssh.useDns` to `services.openssh.settings.UseDns`
+  - `services.openssh.permitRootLogin` to `services.openssh.settings.PermitRootLogin`
+  - `services.openssh.logLevel` to `services.openssh.settings.LogLevel`
+  - `services.openssh.kexAlgorithms` to `services.openssh.settings.KexAlgorithms`
+  - `services.openssh.macs` to `services.openssh.settings.Macs`
+  - `services.openssh.ciphers` to `services.openssh.settings.Ciphers`
+  - `services.openssh.gatewayPorts` to `services.openssh.settings.GatewayPorts`
+
+- `netbox` was updated to 3.5. NixOS' `services.netbox.package` still defaults to 3.3 if `stateVersion` is earlier than 23.05. Please review upstream's breaking changes [for 3.4.0](https://github.com/netbox-community/netbox/releases/tag/v3.4.0) and [for 3.5.0](https://github.com/netbox-community/netbox/releases/tag/v3.5.0), and upgrade NetBox by changing `services.netbox.package`. Database migrations will be run automatically.
+
+- `services.netbox` now support RFC42-style options, through `services.netbox.settings`.
+
 - `services.mastodon` gained a tootctl wrapped named `mastodon-tootctl` similar to `nextcloud-occ` which can be executed from any user and switches to the configured mastodon user with sudo and sources the environment variables.
 
+- DocBook option documentation, which has been deprecated since 22.11, will now cause a warning when documentation is built. Out-of-tree modules should migrate to using CommonMark documentation as outlined in [](#sec-option-declarations) to silence this warning.
+
+  DocBook option documentation support will be removed in the next release and CommonMark will become the default. DocBook option documentation that has not been migrated until then will no longer render properly or cause errors.
+
+- NixOS now defaults to using nsncd (a non-caching reimplementation in Rust) as NSS lookup dispatcher, instead of the buggy and deprecated glibc-provided nscd. If you need to switch back, set `services.nscd.enableNsncd = false`, but please open an issue in nixpkgs so your issue can be fixed.
+
+- `services.borgmatic` now allows for multiple configurations, placed in `/etc/borgmatic.d/`, you can define them with `services.borgmatic.configurations`.
+
+- `service.openafsServer` features a new backup server `pkgs.fabs` as a
+  replacement for openafs's own `buserver`. See
+  [FABS](https://github.com/openafs-contrib/fabs) to check if this is an viable
+  replacement. It stores backups as volume dump files and thus better integrates
+  into contemporary backup solutions.
+
+- `services.maddy` got several updates:
+  - Configuration of users and their credentials using `services.maddy.ensureCredentials`.
+  - Configuration of TLS key and certificate files using `services.maddy.tls`.
+
 - The `dnsmasq` service now takes configuration via the
   `services.dnsmasq.settings` attribute set. The option
   `services.dnsmasq.extraConfig` will be deprecated when NixOS 22.11 reaches
   end of life.
 
+- The `dokuwiki` service is now configured via `services.dokuwiki.sites.<name>.settings` attribute set; `extraConfig` has been removed.
+  The `{aclUse,superUser,disableActions}` attributes have been renamed accordingly. `pluginsConfig` now only accepts an attribute set of booleans.
+  Passing plain PHP is no longer possible.
+  Same applies to `acl` which now also only accepts structured `settings`.
+
+- The `zsh` package changes the way to set environment variables on NixOS systems where `programs.zsh.enable` equals `false`.  It now sources `/etc/set-environment` when reading the system-level `zshenv` file.  Before, it sourced `/etc/profile` when reading the system-level `zprofile` file.
+
+- The `wordpress` service now takes configuration via the `services.wordpress.sites.<name>.settings` attribute set, `extraConfig` is still available to append  additional text to `wp-config.php`.
+
 - To reduce closure size in `nixos/modules/profiles/minimal.nix` profile disabled installation documentations and manuals. Also disabled `logrotate` and `udisks2` services.
 
+- To reduce closure size in `nixos/modules/installer/netboot/netboot-minimal.nix` profile disabled load linux firmwares, pre-installing the complete stdenv and `networking.wireless` service.
+
 - The minimal ISO image now uses the `nixos/modules/profiles/minimal.nix` profile.
 
+- The `ghcWithPackages` and `ghcWithHoogle` wrappers will now also symlink GHC's
+  and all included libraries' documentation to `$out/share/doc` for convenience.
+  If undesired, the old behavior can be restored by overriding the builders with
+  `{ installDocumentation = false; }`.
+
+- The new option `networking.nftables.checkRuleset` controls whether the ruleset is checked for syntax or not during build.  It is `true` by default.  The check might fail because it is in a sandbox environment.  To circumvent this, the ruleset file can be edited using the `networking.nftables.preCheckRuleset` option.
+
 - `mastodon` now supports connection to a remote `PostgreSQL` database.
 
+- `nextcloud` has an option to enable SSE-C in S3.
+
+- `services.peertube` now requires you to specify the secret file `secrets.secretsFile`. It can be generated by running `openssl rand -hex 32`.
+  Before upgrading, read the release notes for PeerTube:
+    - [Release v5.0.0](https://github.com/Chocobozzz/PeerTube/releases/tag/v5.0.0)
+
+  And backup your data.
+
+- `services.chronyd` is now started with additional systemd sandbox/hardening options for better security.
+
+- PostgreSQL has opt-in support for [JIT compilation](https://www.postgresql.org/docs/current/jit-reason.html). It can be enabled like this:
+  ```nix
+  {
+    services.postgresql = {
+      enable = true;
+      enableJIT = true;
+    };
+  }
+  ```
+
+- `services.netdata` offers a `deadlineBeforeStopSec` option which enable users who have netdata instance that takes time to initialize to not have systemd kill them for no reason.
+
+- `services.dhcpcd` service now don't solicit or accept IPv6 Router Advertisements on interfaces that use static IPv6 addresses.
+  If network uses both IPv6 Unique local addresses (ULA) and global IPv6 address auto-configuration with SLAAC, must add the parameter `networking.dhcpcd.IPv6rs = true;`.
+
+- The module `services.headscale` was refactored to be compliant with [RFC 0042](https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md). To be precise, this means that the following things have changed:
+
+  - Most settings has been migrated under [services.headscale.settings](#opt-services.headscale.settings) which is an attribute-set that
+    will be converted into headscale's YAML config format. This means that the configuration from
+    [headscale's example configuration](https://github.com/juanfont/headscale/blob/main/config-example.yaml)
+    can be directly written as attribute-set in Nix within this option.
+
+- `services.kubo` now unmounts `ipfsMountDir` and `ipnsMountDir` even if it is killed unexpectedly when `autoMount` is enabled.
+
+- `nixos/lib/make-disk-image.nix` can now mutate EFI variables, run user-provided EFI firmware or variable templates. This is now extensively documented in the NixOS manual.
+
+- `services.grafana` listens only on localhost by default again. This was changed to upstreams default of `0.0.0.0` by accident in the freeform setting conversion.
+
+- Grafana Tempo has been updated to version 2.0. See the [upstream upgrade guide](https://grafana.com/docs/tempo/latest/release-notes/v2-0/#upgrade-considerations) for migration instructions.
+
 - A new `virtualisation.rosetta` module was added to allow running `x86_64` binaries through [Rosetta](https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment) inside virtualised NixOS guests on Apple silicon. This feature works by default with the [UTM](https://docs.getutm.app/) virtualisation [package](https://search.nixos.org/packages?channel=unstable&show=utm&from=0&size=1&sort=relevance&type=packages&query=utm).
 
 - The new option `users.motdFile` allows configuring a Message Of The Day that can be updated dynamically.
 
+- The `root` package is now built with the `"-Dgnuinstall=ON"` CMake flag, making the output conform the `bin` `lib` `share` layout. In this layout, `tutorials` is under `share/doc/ROOT/`; `cmake`, `font`, `icons`, `js` and `macro` under `share/root`; `Makefile.comp` and `Makefile.config` under `etc/root`.
+
 - Enabling global redirect in `services.nginx.virtualHosts` now allows one to add exceptions with the `locations` option.
 
+- A new option `proxyCachePath` has been added to `services.nginx`. Learn more about proxy_cache_path: <https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path>.
+
+- A new option `recommendedBrotliSettings` has been added to `services.nginx`. Learn more about compression in Brotli format [here](https://github.com/google/ngx_brotli/blob/master/README.md).
+
+- Updated recommended settings in `services.nginx.recommendedGzipSettings`:
+  - Enables gzip compression for only certain proxied requests.
+  - Allow checking and loading of precompressed files.
+  - Updated gzip mime-types.
+  - Increased the minimum length of a response that will be gzipped.
+
+- [Garage](https://garagehq.deuxfleurs.fr/) version is based on [system.stateVersion](options.html#opt-system.stateVersion), existing installations will keep using version 0.7. New installations will use version 0.8. In order to upgrade a Garage cluster, please follow [upstream instructions](https://garagehq.deuxfleurs.fr/documentation/cookbook/upgrading/) and force [services.garage.package](options.html#opt-services.garage.package) or upgrade accordingly [system.stateVersion](options.html#opt-system.stateVersion).
+
+- Nebula now supports the `services.nebula.networks.<name>.isRelay` and `services.nebula.networks.<name>.relays` configuration options for setting up or allowing traffic relaying. See the [announcement](https://www.defined.net/blog/announcing-relay-support-in-nebula/) for more details about relays.
+
+- `hip` has been separated into `hip`, `hip-common` and `hipcc`.
+
+- `services.nginx.recommendedProxySettings` now removes the `Connection` header preventing clients from closing backend connections.
+
 - Resilio sync secret keys can now be provided using a secrets file at runtime, preventing these secrets from ending up in the Nix store.
 
+- The `firewall` and `nat` module now has a nftables based implementation. Enable `networking.nftables` to use it.
+
 - The `services.fwupd` module now allows arbitrary daemon settings to be configured in a structured manner ([`services.fwupd.daemonSettings`](#opt-services.fwupd.daemonSettings)).
 
+- `services.xserver.desktopManager.plasma5.phononBackend` now defaults to vlc according to [upstrean recommendation](https://community.kde.org/Distributions/Packaging_Recommendations#Non-Plasma_packages)
+
+- The `zramSwap` is now implemented with `zram-generator`, and the option `zramSwap.numDevices` for using ZRAM devices as general purpose ephemeral block devices has been removed.
+
+- As Singularity has renamed to [Apptainer](https://apptainer.org/news/community-announcement-20211130)
+  to distinguish from [an un-renamed fork by Sylabs Inc.](https://sylabs.io/2021/05/singularity-community-edition),
+  there are now two packages of Singularity/Apptainer:
+  * `apptainer`: From `github.com/apptainer/apptainer`, which is the new repo after renaming.
+  * `singularity`: From `github.com/sylabs/singularity`, which is the fork by Sylabs Inc..
+
+  `programs.singularity` got a new `package` option to specify which package to use.
+
+  `singularity-tools.buildImage` got a new input argument `singularity` to specify which package to use.
+
+- The new option `programs.singularity.enableFakeroot`, if set to `true`, provides `--fakeroot` support for `apptainer` and `singularity`.
+
 - The `unifi-poller` package and corresponding NixOS module have been renamed to `unpoller` to match upstream.
 
+- The `rtsp-simple-server` package and corresponding NixOS module have been renamed to `mediamtx` to match upstream.
+
 - The new option `services.tailscale.useRoutingFeatures` controls various settings for using Tailscale features like exit nodes and subnet routers. If you wish to use your machine as an exit node, you can set this setting to `server`, otherwise if you wish to use an exit node you can set this setting to `client`. The strict RPF warning has been removed as the RPF will be loosened automatically based on the value of this setting.
+
+- `openjdk` from version 11 and above is not build with `openjfx` (i.e.: JavaFX) support by default anymore. You can re-enable it by overriding, e.g.: `openjdk11.override { enableJavaFX = true; };`.
+
+- [Xastir](https://xastir.org/index.php/Main_Page) can now access AX.25 interfaces via the `libax25` package.
+
+- `nixos-version` now accepts `--configuration-revision` to display more information about the current generation revision
+
+- The option `services.nomad.extraSettingsPlugins` has been fixed to allow more than one plugin in the path.
+
+- The option `services.prometheus.exporters.pihole.interval` does not exist anymore and has been removed.
+
+- The option `services.gpsd.device` has been replaced with
+  `services.gpsd.devices`, which supports multiple devices.
+
+- `k3s` can now be configured with an EnvironmentFile for its systemd service, allowing secrets to be provided without ending up in the Nix Store.
+
+- `gitea` module options have been changed to be RFC042 conforming (i.e. some options were moved to be located under `services.gitea.settings`)
+
+- `boot.initrd.luks.device.<name>` has a new `tryEmptyPassphrase` option, this is useful for OEM's who need to install an encrypted disk with a future settable passphrase
+
+- Lisp gained a [manual section](https://nixos.org/manual/nixpkgs/stable/#lisp), documenting a new and backwards incompatible interface. The previous interface will be removed in a future release.
+
+- The `bind` module now allows the per-zone `allow-query` setting to be configured (previously it was hard-coded to `any`; it still defaults to `any` to retain compatibility).
+
+- `make-disk-image` handles `contents` arguments that are directories better, fixing a bug where it used to put them in a subdirectory of the intended `target`.
+
+## Detailed migration information {#sec-release-23.05-migration}
+
+### Pipewire configuration overrides {#sec-release-23.05-migration-pipewire}
+
+#### Why this change? {#sec-release-23.05-migration-pipewire-why}
+
+The Pipewire config semantics don't really match the NixOS module semantics, so it's extremely awkward to override the default config, especially when lists are involved. Vendoring the configuration files in nixpkgs also creates unnecessary maintenance overhead.
+
+Also, upstream added a lot of accomodations to allow doing most of the things you'd want to do with a config edit in better ways.
+
+#### Migrating your configuration {#sec-release-23.05-migration-pipewire-how}
+
+Compare your settings to [the defaults](https://gitlab.freedesktop.org/pipewire/pipewire/-/tree/master/src/daemon) and where your configuration differs from them.
+
+Then, create a drop-in JSON file in `/etc/pipewire/<config file name>.d/99-custom.conf` (the actual filename can be anything) and migrate your changes to it according to the following sections.
+
+Repeat for every file you've modified, changing the directory name accordingly.
+
+#### Things you can just copy over {#sec-release-23.05-migration-pipewire-simple}
+
+If you are:
+
+- setting properties via `*.properties`
+- loading a new module to `context.modules`
+- creating new objects with `context.objects`
+- declaring SPA libraries with `context.spa-libs`
+- running custom commands with `context.exec`
+- adding new rules with `*.rules`
+- running custom PulseAudio commands with `pulse.cmd`
+
+Simply move the definitions into the drop-in.
+
+Note that the use of `context.exec` is not recommended and other methods of running your thing are likely a better option.
+
+```json
+{
+  "context.properties": {
+    "your.property.name": "your.property.value"
+  },
+  "context.modules": [
+    { "name": "libpipewire-module-my-cool-thing" }
+  ],
+  "context.objects": [
+    { "factory": { ... } }
+  ],
+  "alsa.rules": [
+    { "matches: { ... }, "actions": { ... } }
+  ]
+}
+```
+
+#### Removing a module from `context.modules` {#sec-release-23.05-migration-pipewire-removing-modules}
+
+Look for an option to disable it via `context.properties` (`"module.x11.bell": "false"` is likely the most common use case here).
+If one is not available, proceed to [Nuclear option](#sec-release-23.05-migration-pipewire).
+
+#### Modifying a module's parameters in `context.modules` {#sec-release-23.05-migration-pipewire-modifying-modules}
+
+For most modules (e.g. `libpipewire-module-rt`) it's enough to load the module again with the new arguments, e.g.:
+
+```json
+{
+  "context.modules": [
+    {
+      "name": "libpipewire-module-rt",
+      "args": {
+        "rt.prio": 90
+      }
+    }
+  ]
+}
+```
+
+Note that `module-rt` specifically will generally use the highest values available by default, so setting limits on the `pipewire` systemd service is preferable to reloading.
+
+If reloading the module is not an option, proceed to [Nuclear option](#sec-release-23.05-migration-pipewire).
+
+#### Nuclear option {#sec-release-23.05-migration-pipewire-nuclear}
+If all else fails, you can still manually copy the contents of the default configuration file
+from `${pkgs.pipewire.lib}/share/pipewire` to `/etc/pipewire` and edit it to fully override the default.
+However, this should be done only as a last resort. Please talk to the Pipewire maintainers if you ever need to do this.
diff --git a/nixos/doc/manual/shell.nix b/nixos/doc/manual/shell.nix
deleted file mode 100644
index e5ec9b8f97f..00000000000
--- a/nixos/doc/manual/shell.nix
+++ /dev/null
@@ -1,8 +0,0 @@
-let
-  pkgs = import ../../.. { };
-in
-pkgs.mkShell {
-  name = "nixos-manual";
-
-  packages = with pkgs; [ xmlformat jing xmloscopy ruby ];
-}
diff --git a/nixos/doc/varlistentry-fixer.rb b/nixos/doc/varlistentry-fixer.rb
deleted file mode 100755
index 02168016b55..00000000000
--- a/nixos/doc/varlistentry-fixer.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/usr/bin/env ruby
-
-# This script is written intended as a living, evolving tooling
-# to fix oopsies within the docbook documentation.
-#
-# This is *not* a formatter. It, instead, handles some known cases
-# where something bad happened, and fixing it manually is tedious.
-#
-# Read the code to see the different cases it handles.
-#
-# ALWAYS `make format` after fixing with this!
-# ALWAYS read the changes, this tool isn't yet proven to be always right.
-
-require "rexml/document"
-include REXML
-
-if ARGV.length < 1 then
-  $stderr.puts "Needs a filename."
-  exit 1
-end
-
-filename = ARGV.shift
-doc = Document.new(File.open(filename))
-
-$touched = false
-
-# Fixing varnames having a sibling element without spacing.
-# This is to fix an initial `xmlformat` issue where `term`
-# would mangle as spaces.
-#
-#   <varlistentry>
-#    <term><varname>types.separatedString</varname><replaceable>sep</replaceable> <----
-#    </term>
-#    ...
-#
-# Generates: types.separatedStringsep
-#                               ^^^^
-#
-# <varlistentry xml:id='fun-makeWrapper'>
-#  <term>
-#   <function>makeWrapper</function><replaceable>executable</replaceable><replaceable>wrapperfile</replaceable><replaceable>args</replaceable>  <----
-#  </term>
-#
-# Generates: makeWrapperexecutablewrapperfileargs
-#                     ^^^^      ^^^^    ^^  ^^
-#
-#    <term>
-#     <option>--option</option><replaceable>name</replaceable><replaceable>value</replaceable> <-----
-#    </term>
-#
-# Generates: --optionnamevalue
-#                   ^^  ^^
-doc.elements.each("//varlistentry/term") do |term|
-  ["varname", "function", "option", "replaceable"].each do |prev_name|
-    term.elements.each(prev_name) do |el|
-      if el.next_element and
-          el.next_element.name == "replaceable" and
-          el.next_sibling_node.class == Element
-        then
-        $touched = true
-        term.insert_after(el, Text.new(" "))
-      end
-    end
-  end
-end
-
-
-
-#  <cmdsynopsis>
-#   <command>nixos-option</command>
-#   <arg>
-#    <option>-I</option><replaceable>path</replaceable>        <------
-#   </arg>
-#
-# Generates: -Ipath
-#             ^^
-doc.elements.each("//cmdsynopsis/arg") do |term|
-  ["option", "replaceable"].each do |prev_name|
-    term.elements.each(prev_name) do |el|
-      if el.next_element and
-        el.next_element.name == "replaceable" and
-        el.next_sibling_node.class == Element
-      then
-        $touched = true
-        term.insert_after(el, Text.new(" "))
-      end
-    end
-  end
-end
-
-#  <cmdsynopsis>
-#   <arg>
-#    <group choice='req'>
-#    <arg choice='plain'>
-#     <option>--profile-name</option>
-#    </arg>
-#
-#    <arg choice='plain'>
-#     <option>-p</option>
-#    </arg>
-#     </group><replaceable>name</replaceable>   <----
-#   </arg>
-#
-# Generates: [{--profile-name | -p }name]
-#                                   ^^^^
-doc.elements.each("//cmdsynopsis/arg") do |term|
-  ["group"].each do |prev_name|
-    term.elements.each(prev_name) do |el|
-      if el.next_element and
-        el.next_element.name == "replaceable" and
-        el.next_sibling_node.class == Element
-      then
-        $touched = true
-        term.insert_after(el, Text.new(" "))
-      end
-    end
-  end
-end
-
-
-if $touched then
-  doc.context[:attribute_quote] = :quote
-  doc.write(output: File.open(filename, "w"))
-end
diff --git a/nixos/doc/xmlformat.conf b/nixos/doc/xmlformat.conf
deleted file mode 100644
index c3f39c7fd81..00000000000
--- a/nixos/doc/xmlformat.conf
+++ /dev/null
@@ -1,72 +0,0 @@
-#
-# DocBook Configuration file for "xmlformat"
-# see http://www.kitebird.com/software/xmlformat/
-# 10 Sept. 2004
-#
-
-# Only block elements
-ackno address appendix article biblioentry bibliography bibliomixed \
-biblioset blockquote book bridgehead callout calloutlist caption caution \
-chapter chapterinfo classsynopsis cmdsynopsis colophon constraintdef \
-constructorsynopsis dedication destructorsynopsis entry epigraph equation example \
-figure formalpara funcsynopsis glossary glossdef glossdiv glossentry glosslist \
-glosssee glossseealso graphic graphicco highlights imageobjectco important \
-index indexdiv indexentry indexinfo info informalequation informalexample \
-informalfigure informaltable legalnotice literallayout lot lotentry mediaobject \
-mediaobjectco msgmain msgset note orderedlist para part preface primaryie \
-procedure qandadiv qandaentry qandaset refentry refentrytitle reference \
-refnamediv refsect1 refsect2 refsect3 refsection revhistory screenshot sect1 \
-sect2 sect3 sect4 sect5 section seglistitem set setindex sidebar simpara \
-simplesect step substeps synopfragment synopsis table term title \
-toc variablelist varlistentry warning itemizedlist listitem \
-footnote colspec partintro row simplelist subtitle tbody tgroup thead tip
-  format      block
-  normalize   no
-
-
-#appendix bibliography chapter glossary preface reference
-#  element-break   3
-
-sect1 section
-  element-break   2
-
-
-#
-para abstract
-  format       block
-  entry-break  1
-  exit-break   1
-  normalize    yes
-
-title
-  format       block
-  normalize = yes
-  entry-break = 0
-  exit-break = 0
-
-# Inline elements
-abbrev accel acronym action application citation citebiblioid citerefentry citetitle \
-classname co code command computeroutput constant country database date email emphasis \
-envar errorcode errorname errortext errortype exceptionname fax filename \
-firstname firstterm footnoteref foreignphrase funcdef funcparams function \
-glossterm group guibutton guiicon guilabel guimenu guimenuitem guisubmenu \
-hardware holder honorific indexterm inlineequation inlinegraphic inlinemediaobject \
-interface interfacename \
-keycap keycode keycombo keysym lineage link literal manvolnum markup medialabel \
-menuchoice methodname methodparam modifier mousebutton olink ooclass ooexception \
-oointerface option optional otheraddr othername package paramdef parameter personname \
-phrase pob postcode productname prompt property quote refpurpose replaceable \
-returnvalue revnumber sgmltag state street structfield structname subscript \
-superscript surname symbol systemitem token trademark type ulink userinput \
-uri varargs varname void wordasword xref year mathphrase member tag
-  format       inline
-
-programlisting screen
-  format       verbatim
-  entry-break = 0
-  exit-break = 0
-
-# This is needed so that the spacing inside those tags is kept.
-term cmdsynopsis arg
-  normalize yes
-  format    block
diff --git a/nixos/lib/eval-cacheable-options.nix b/nixos/lib/eval-cacheable-options.nix
index c3ba2ce6637..d26967ebe09 100644
--- a/nixos/lib/eval-cacheable-options.nix
+++ b/nixos/lib/eval-cacheable-options.nix
@@ -33,6 +33,7 @@ let
     ];
     specialArgs = {
       inherit config pkgs utils;
+      class = "nixos";
     };
   };
   docs = import "${nixosPath}/doc/manual" {
diff --git a/nixos/lib/eval-config-minimal.nix b/nixos/lib/eval-config-minimal.nix
index d45b9ffd426..03638912197 100644
--- a/nixos/lib/eval-config-minimal.nix
+++ b/nixos/lib/eval-config-minimal.nix
@@ -38,6 +38,7 @@ let
   #       is experimental.
   lib.evalModules {
     inherit prefix modules;
+    class = "nixos";
     specialArgs = {
       modulesPath = builtins.toString ../modules;
     } // specialArgs;
diff --git a/nixos/lib/make-channel.nix b/nixos/lib/make-channel.nix
index 9b920b989fc..0a511468fb2 100644
--- a/nixos/lib/make-channel.nix
+++ b/nixos/lib/make-channel.nix
@@ -23,7 +23,7 @@ pkgs.releaseTools.makeSourceTarball {
     cp -prd . ../$releaseName
     chmod -R u+w ../$releaseName
     ln -s . ../$releaseName/nixpkgs # hack to make ‘<nixpkgs>’ work
-    NIX_STATE_DIR=$TMPDIR nix-env -f ../$releaseName/default.nix -qaP --meta --xml \* > /dev/null
+    NIX_STATE_DIR=$TMPDIR nix-env -f ../$releaseName/default.nix -qaP --meta --show-trace --xml \* > /dev/null
     cd ..
     chmod -R u+w $releaseName
     tar cfJ $out/tarballs/$releaseName.tar.xz $releaseName
diff --git a/nixos/lib/make-disk-image.nix b/nixos/lib/make-disk-image.nix
index e784ec9e677..db53bb98ee4 100644
--- a/nixos/lib/make-disk-image.nix
+++ b/nixos/lib/make-disk-image.nix
@@ -1,3 +1,85 @@
+/* Technical details
+
+`make-disk-image` has a bit of magic to minimize the amount of work to do in a virtual machine.
+
+It relies on the [LKL (Linux Kernel Library) project](https://github.com/lkl/linux) which provides Linux kernel as userspace library.
+
+The Nix-store only image only need to run LKL tools to produce an image and will never spawn a virtual machine, whereas full images will always require a virtual machine, but also use LKL.
+
+### Image preparation phase
+
+Image preparation phase will produce the initial image layout in a folder:
+
+- devise a root folder based on `$PWD`
+- prepare the contents by copying and restoring ACLs in this root folder
+- load in the Nix store database all additional paths computed by `pkgs.closureInfo` in a temporary Nix store
+- run `nixos-install` in a temporary folder
+- transfer from the temporary store the additional paths registered to the installed NixOS
+- compute the size of the disk image based on the apparent size of the root folder
+- partition the disk image using the corresponding script according to the partition table type
+- format the partitions if needed
+- use `cptofs` (LKL tool) to copy the root folder inside the disk image
+
+At this step, the disk image already contains the Nix store, it now only needs to be converted to the desired format to be used.
+
+### Image conversion phase
+
+Using `qemu-img`, the disk image is converted from a raw format to the desired format: qcow2(-compressed), vdi, vpc.
+
+### Image Partitioning
+
+#### `none`
+
+No partition table layout is written. The image is a bare filesystem image.
+
+#### `legacy`
+
+The image is partitioned using MBR. There is one primary ext4 partition starting at 1 MiB that fills the rest of the disk image.
+
+This partition layout is unsuitable for UEFI.
+
+#### `legacy+gpt`
+
+This partition table type uses GPT and:
+
+- create a "no filesystem" partition from 1MiB to 2MiB ;
+- set `bios_grub` flag on this "no filesystem" partition, which marks it as a [GRUB BIOS partition](https://www.gnu.org/software/parted/manual/html_node/set.html) ;
+- create a primary ext4 partition starting at 2MiB and extending to the full disk image ;
+- perform optimal alignments checks on each partition
+
+This partition layout is unsuitable for UEFI boot, because it has no ESP (EFI System Partition) partition. It can work with CSM (Compatibility Support Module) which emulates legacy (BIOS) boot for UEFI.
+
+#### `efi`
+
+This partition table type uses GPT and:
+
+- creates an FAT32 ESP partition from 8MiB to specified `bootSize` parameter (256MiB by default), set it bootable ;
+- creates an primary ext4 partition starting after the boot partition and extending to the full disk image
+
+#### `hybrid`
+
+This partition table type uses GPT and:
+
+- creates a "no filesystem" partition from 0 to 1MiB, set `bios_grub` flag on it ;
+- creates an FAT32 ESP partition from 8MiB to specified `bootSize` parameter (256MiB by default), set it bootable ;
+- creates a primary ext4 partition starting after the boot one and extending to the full disk image
+
+This partition could be booted by a BIOS able to understand GPT layouts and recognizing the MBR at the start.
+
+### How to run determinism analysis on results?
+
+Build your derivation with `--check` to rebuild it and verify it is the same.
+
+If it fails, you will be left with two folders with one having `.check`.
+
+You can use `diffoscope` to see the differences between the folders.
+
+However, `diffoscope` is currently not able to diff two QCOW2 filesystems, thus, it is advised to use raw format.
+
+Even if you use raw disks, `diffoscope` cannot diff the partition table and partitions recursively.
+
+To solve this, you can run `fdisk -l $image` and generate `dd if=$image of=$image-p$i.raw skip=$start count=$sectors` for each `(start, sectors)` listed in the `fdisk` output. Now, you will have each partition as a separate file and you can compare them in pairs.
+*/
 { pkgs
 , lib
 
@@ -47,6 +129,18 @@
 , # Whether to invoke `switch-to-configuration boot` during image creation
   installBootLoader ? true
 
+, # Whether to output have EFIVARS available in $out/efi-vars.fd and use it during disk creation
+  touchEFIVars ? false
+
+, # OVMF firmware derivation
+  OVMF ? pkgs.OVMF.fd
+
+, # EFI firmware
+  efiFirmware ? OVMF.firmware
+
+, # EFI variables
+  efiVariables ? OVMF.variables
+
 , # The root file system type.
   fsType ? "ext4"
 
@@ -60,6 +154,9 @@
 , # Shell code executed after the VM has finished.
   postVM ? ""
 
+, # Guest memory size
+  memSize ? 1024
+
 , # Copy the contents of the Nix store to the root of the image and
   # skip further setup. Incompatible with `contents`,
   # `installBootLoader` and `configFile`.
@@ -70,6 +167,22 @@
 , # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
   format ? "raw"
 
+  # Whether to fix:
+  #   - GPT Disk Unique Identifier (diskGUID)
+  #   - GPT Partition Unique Identifier: depends on the layout, root partition UUID can be controlled through `rootGPUID` option
+  #   - GPT Partition Type Identifier: fixed according to the layout, e.g. ESP partition, etc. through `parted` invocation.
+  #   - Filesystem Unique Identifier when fsType = ext4 for *root partition*.
+  # BIOS/MBR support is "best effort" at the moment.
+  # Boot partitions may not be deterministic.
+  # Also, to fix last time checked of the ext4 partition if fsType = ext4.
+, deterministic ? true
+
+  # GPT Partition Unique Identifier for root partition.
+, rootGPUID ? "F222513B-DED1-49FA-B591-20CE86A2FE7F"
+  # When fsType = ext4, this is the root Filesystem Unique Identifier.
+  # TODO: support other filesystems someday.
+, rootFSUID ? (if fsType == "ext4" then rootGPUID else null)
+
 , # Whether a nix channel based on the current source tree should be
   # made available inside the image. Useful for interactive use of nix
   # utils, but changes the hash of the image when the sources are
@@ -80,15 +193,18 @@
   additionalPaths ? []
 }:
 
-assert partitionTableType == "legacy" || partitionTableType == "legacy+gpt" || partitionTableType == "efi" || partitionTableType == "hybrid" || partitionTableType == "none";
-# We use -E offset=X below, which is only supported by e2fsprogs
-assert partitionTableType != "none" -> fsType == "ext4";
+assert (lib.assertOneOf "partitionTableType" partitionTableType [ "legacy" "legacy+gpt" "efi" "hybrid" "none" ]);
+assert (lib.assertMsg (fsType == "ext4" && deterministic -> rootFSUID != null) "In deterministic mode with a ext4 partition, rootFSUID must be non-null, by default, it is equal to rootGPUID.");
+  # We use -E offset=X below, which is only supported by e2fsprogs
+assert (lib.assertMsg (partitionTableType != "none" -> fsType == "ext4") "to produce a partition table, we need to use -E offset flag which is support only for fsType = ext4");
+assert (lib.assertMsg (touchEFIVars -> partitionTableType == "hybrid" || partitionTableType == "efi" || partitionTableType == "legacy+gpt") "EFI variables can be used only with a partition table of type: hybrid, efi or legacy+gpt.");
+  # If only Nix store image, then: contents must be empty, configFile must be unset, and we should no install bootloader.
+assert (lib.assertMsg (onlyNixStore -> contents == [] && configFile == null && !installBootLoader) "In a only Nix store image, the contents must be empty, no configuration must be provided and no bootloader should be installed.");
 # Either both or none of {user,group} need to be set
-assert lib.all
+assert (lib.assertMsg (lib.all
          (attrs: ((attrs.user  or null) == null)
               == ((attrs.group or null) == null))
-         contents;
-assert onlyNixStore -> contents == [] && configFile == null && !installBootLoader;
+        contents) "Contents of the disk image should set none of {user, group} or both at the same time.");
 
 with lib;
 
@@ -127,6 +243,14 @@ let format' = format; in let
         mkpart primary ext4 2MB -1 \
         align-check optimal 2 \
         print
+      ${optionalString deterministic ''
+          sgdisk \
+          --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
+          --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
+          --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
+          --partition-guid=3:${rootGPUID} \
+          $diskImage
+      ''}
     '';
     efi = ''
       parted --script $diskImage -- \
@@ -134,6 +258,13 @@ let format' = format; in let
         mkpart ESP fat32 8MiB ${bootSize} \
         set 1 boot on \
         mkpart primary ext4 ${bootSize} -1
+      ${optionalString deterministic ''
+          sgdisk \
+          --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
+          --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
+          --partition-guid=2:${rootGPUID} \
+          $diskImage
+      ''}
     '';
     hybrid = ''
       parted --script $diskImage -- \
@@ -143,10 +274,20 @@ let format' = format; in let
         mkpart no-fs 0 1024KiB \
         set 2 bios_grub on \
         mkpart primary ext4 ${bootSize} -1
+      ${optionalString deterministic ''
+          sgdisk \
+          --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C \
+          --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
+          --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
+          --partition-guid=3:${rootGPUID} \
+          $diskImage
+      ''}
     '';
     none = "";
   }.${partitionTableType};
 
+  useEFIBoot = touchEFIVars;
+
   nixpkgs = cleanSource pkgs.path;
 
   # FIXME: merge with channel.nix / make-channel.nix.
@@ -171,7 +312,9 @@ let format' = format; in let
       config.system.build.nixos-enter
       nix
       systemdMinimal
-    ] ++ stdenv.initialPath);
+    ]
+    ++ lib.optional deterministic gptfdisk
+    ++ stdenv.initialPath);
 
   # I'm preserving the line below because I'm going to search for it across nixpkgs to consolidate
   # image building logic. The comment right below this now appears in 4 different places in nixpkgs :)
@@ -259,11 +402,16 @@ let format' = format; in let
         done
       else
         mkdir -p $root/$(dirname $target)
-        if ! [ -e $root/$target ]; then
-          rsync $rsync_flags $source $root/$target
-        else
+        if [ -e $root/$target ]; then
           echo "duplicate entry $target -> $source"
           exit 1
+        elif [ -d $source ]; then
+          # Append a slash to the end of source to get rsync to copy the
+          # directory _to_ the target instead of _inside_ the target.
+          # (See `man rsync`'s note on a trailing slash.)
+          rsync $rsync_flags $source/ $root/$target
+        else
+          rsync $rsync_flags $source $root/$target
         fi
       fi
     done
@@ -368,20 +516,35 @@ let format' = format; in let
     diskImage=$out/${filename}
   '';
 
+  createEFIVars = ''
+    efiVars=$out/efi-vars.fd
+    cp ${efiVariables} $efiVars
+    chmod 0644 $efiVars
+  '';
+
   buildImage = pkgs.vmTools.runInLinuxVM (
     pkgs.runCommand name {
-      preVM = prepareImage;
+      preVM = prepareImage + lib.optionalString touchEFIVars createEFIVars;
       buildInputs = with pkgs; [ util-linux e2fsprogs dosfstools ];
       postVM = moveOrConvertImage + postVM;
-      memSize = 1024;
+      QEMU_OPTS =
+        concatStringsSep " " (lib.optional useEFIBoot "-drive if=pflash,format=raw,unit=0,readonly=on,file=${efiFirmware}"
+        ++ lib.optionals touchEFIVars [
+          "-drive if=pflash,format=raw,unit=1,file=$efiVars"
+        ]
+      );
+      inherit memSize;
     } ''
       export PATH=${binPath}:$PATH
 
       rootDisk=${if partitionTableType != "none" then "/dev/vda${rootPartition}" else "/dev/vda"}
 
-      # Some tools assume these exist
-      ln -s vda /dev/xvda
-      ln -s vda /dev/sda
+      # It is necessary to set root filesystem unique identifier in advance, otherwise
+      # bootloader might get the wrong one and fail to boot.
+      # At the end, we reset again because we want deterministic timestamps.
+      ${optionalString (fsType == "ext4" && deterministic) ''
+        tune2fs -T now ${optionalString deterministic "-U ${rootFSUID}"} -c 0 -i 0 $rootDisk
+      ''}
       # make systemd-boot find ESP without udev
       mkdir /dev/block
       ln -s /dev/vda1 /dev/block/254:1
@@ -396,6 +559,8 @@ let format' = format; in let
         mkdir -p /mnt/boot
         mkfs.vfat -n ESP /dev/vda1
         mount /dev/vda1 /mnt/boot
+
+        ${optionalString touchEFIVars "mount -t efivarfs efivarfs /sys/firmware/efi/efivars"}
       ''}
 
       # Install a configuration.nix
@@ -405,7 +570,13 @@ let format' = format; in let
       ''}
 
       ${lib.optionalString installBootLoader ''
-        # Set up core system link, GRUB, etc.
+        # In this throwaway resource, we only have /dev/vda, but the actual VM may refer to another disk for bootloader, e.g. /dev/vdb
+        # Use this option to create a symlink from vda to any arbitrary device you want.
+        ${optionalString (config.boot.loader.grub.device != "/dev/vda") ''
+            ln -s /dev/vda ${config.boot.loader.grub.device}
+        ''}
+
+        # Set up core system link, bootloader (sd-boot, GRUB, uboot, etc.), etc.
         NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root $mountPoint -- /nix/var/nix/profiles/system/bin/switch-to-configuration boot
 
         # The above scripts will generate a random machine-id and we don't want to bake a single ID into all our images
@@ -432,8 +603,12 @@ let format' = format; in let
       # Make sure resize2fs works. Note that resize2fs has stricter criteria for resizing than a normal
       # mount, so the `-c 0` and `-i 0` don't affect it. Setting it to `now` doesn't produce deterministic
       # output, of course, but we can fix that when/if we start making images deterministic.
+      # In deterministic mode, this is fixed to 1970-01-01 (UNIX timestamp 0).
+      # This two-step approach is necessary otherwise `tune2fs` will want a fresher filesystem to perform
+      # some changes.
       ${optionalString (fsType == "ext4") ''
-        tune2fs -T now -c 0 -i 0 $rootDisk
+        tune2fs -T now ${optionalString deterministic "-U ${rootFSUID}"} -c 0 -i 0 $rootDisk
+        ${optionalString deterministic "tune2fs -f -T 19700101 $rootDisk"}
       ''}
     ''
   );
diff --git a/nixos/lib/make-multi-disk-zfs-image.nix b/nixos/lib/make-multi-disk-zfs-image.nix
index f9046a485a7..ecbde44971a 100644
--- a/nixos/lib/make-multi-disk-zfs-image.nix
+++ b/nixos/lib/make-multi-disk-zfs-image.nix
@@ -73,6 +73,9 @@
 , # Shell code executed after the VM has finished.
   postVM ? ""
 
+, # Guest memory size
+  memSize ? 1024
+
 , name ? "nixos-disk-image"
 
 , # Disk image format, one of qcow2, qcow2-compressed, vdi, vpc, raw.
@@ -242,6 +245,7 @@ let
       {
         QEMU_OPTS = "-drive file=$bootDiskImage,if=virtio,cache=unsafe,werror=report"
          + " -drive file=$rootDiskImage,if=virtio,cache=unsafe,werror=report";
+         inherit memSize;
         preVM = ''
           PATH=$PATH:${pkgs.qemu_kvm}/bin
           mkdir $out
diff --git a/nixos/lib/make-options-doc/default.nix b/nixos/lib/make-options-doc/default.nix
index a3436caad8f..a2385582a01 100644
--- a/nixos/lib/make-options-doc/default.nix
+++ b/nixos/lib/make-options-doc/default.nix
@@ -41,6 +41,7 @@
 # characteristics but (hopefully) indistinguishable output.
 , allowDocBook ? true
 # whether lib.mdDoc is required for descriptions to be read as markdown.
+# !!! when this is eventually flipped to true, `lib.doRename` should also default to emitting Markdown
 , markdownByDefault ? false
 }:
 
@@ -77,50 +78,44 @@ let
           title = args.title or null;
           name = args.name or (lib.concatStringsSep "." args.path);
         in ''
-          <listitem>
-            <para>
-              <link xlink:href="https://search.nixos.org/packages?show=${name}&amp;sort=relevance&amp;query=${name}">
-                <literal>${lib.optionalString (title != null) "${title} aka "}pkgs.${name}</literal>
-              </link>
-            </para>
-            ${lib.optionalString (args ? comment) "<para>${args.comment}</para>"}
-          </listitem>
+          - [${lib.optionalString (title != null) "${title} aka "}`pkgs.${name}`](
+              https://search.nixos.org/packages?show=${name}&sort=relevance&query=${name}
+            )${
+              lib.optionalString (args ? comment) "\n\n  ${args.comment}"
+            }
         '';
-    in "<itemizedlist>${lib.concatStringsSep "\n" (map (p: describe (unpack p)) packages)}</itemizedlist>";
+    in lib.concatMapStrings (p: describe (unpack p)) packages;
 
   optionsNix = builtins.listToAttrs (map (o: { name = o.name; value = removeAttrs o ["name" "visible" "internal"]; }) optionsList);
 
 in rec {
   inherit optionsNix;
 
-  optionsAsciiDoc = pkgs.runCommand "options.adoc" {} ''
-    ${pkgs.python3Minimal}/bin/python ${./generateDoc.py} \
-      --format asciidoc \
+  optionsAsciiDoc = pkgs.runCommand "options.adoc" {
+    nativeBuildInputs = [ pkgs.nixos-render-docs ];
+  } ''
+    nixos-render-docs -j $NIX_BUILD_CORES options asciidoc \
+      --manpage-urls ${pkgs.path + "/doc/manpage-urls.json"} \
+      --revision ${lib.escapeShellArg revision} \
       ${optionsJSON}/share/doc/nixos/options.json \
-      > $out
+      $out
   '';
 
-  optionsCommonMark = pkgs.runCommand "options.md" {} ''
-    ${pkgs.python3Minimal}/bin/python ${./generateDoc.py} \
-      --format commonmark \
+  optionsCommonMark = pkgs.runCommand "options.md" {
+    nativeBuildInputs = [ pkgs.nixos-render-docs ];
+  } ''
+    nixos-render-docs -j $NIX_BUILD_CORES options commonmark \
+      --manpage-urls ${pkgs.path + "/doc/manpage-urls.json"} \
+      --revision ${lib.escapeShellArg revision} \
       ${optionsJSON}/share/doc/nixos/options.json \
-      > $out
+      $out
   '';
 
   optionsJSON = pkgs.runCommand "options.json"
     { meta.description = "List of NixOS options in JSON format";
       nativeBuildInputs = [
         pkgs.brotli
-        (let
-          # python3Minimal can't be overridden with packages on Darwin, due to a missing framework.
-          # Instead of modifying stdenv, we take the easy way out, since most people on Darwin will
-          # just be hacking on the Nixpkgs manual (which also uses make-options-doc).
-          python = if pkgs.stdenv.isDarwin then pkgs.python3 else pkgs.python3Minimal;
-          self = (python.override {
-            inherit self;
-            includeSiteCustomize = true;
-           });
-         in self.withPackages (p: [ p.mistune ]))
+        pkgs.python3Minimal
       ];
       options = builtins.toFile "options.json"
         (builtins.unsafeDiscardStringContext (builtins.toJSON optionsNix));
@@ -136,10 +131,10 @@ in rec {
       dst=$out/share/doc/nixos
       mkdir -p $dst
 
+      TOUCH_IF_DB=$dst/.used-docbook \
       python ${./mergeJSON.py} \
         ${lib.optionalString warningsAreErrors "--warnings-are-errors"} \
-        ${lib.optionalString (! allowDocBook) "--error-on-docbook"} \
-        ${lib.optionalString markdownByDefault "--markdown-by-default"} \
+        ${if allowDocBook then "--warn-on-docbook" else "--error-on-docbook"} \
         $baseJSON $options \
         > $dst/options.json
 
@@ -150,21 +145,30 @@ in rec {
       echo "file json-br $dst/options.json.br" >> $out/nix-support/hydra-build-products
     '';
 
-  # Convert options.json into an XML file.
-  # The actual generation of the xml file is done in nix purely for the convenience
-  # of not having to generate the xml some other way
-  optionsXML = pkgs.runCommand "options.xml" {} ''
-    export NIX_STORE_DIR=$TMPDIR/store
-    export NIX_STATE_DIR=$TMPDIR/state
-    ${pkgs.nix}/bin/nix-instantiate \
-      --eval --xml --strict ${./optionsJSONtoXML.nix} \
-      --argstr file ${optionsJSON}/share/doc/nixos/options.json \
-      > "$out"
+  optionsUsedDocbook = pkgs.runCommand "options-used-docbook" {} ''
+    if [ -e ${optionsJSON}/share/doc/nixos/.used-docbook ]; then
+      echo 1
+    else
+      echo 0
+    fi >"$out"
   '';
 
-  optionsDocBook = pkgs.runCommand "options-docbook.xml" {} ''
-    optionsXML=${optionsXML}
-    if grep /nixpkgs/nixos/modules $optionsXML; then
+  optionsDocBook = pkgs.runCommand "options-docbook.xml" {
+    nativeBuildInputs = [
+      pkgs.nixos-render-docs
+    ];
+  } ''
+    nixos-render-docs -j $NIX_BUILD_CORES options docbook \
+      --manpage-urls ${pkgs.path + "/doc/manpage-urls.json"} \
+      --revision ${lib.escapeShellArg revision} \
+      --document-type ${lib.escapeShellArg documentType} \
+      --varlist-id ${lib.escapeShellArg variablelistId} \
+      --id-prefix ${lib.escapeShellArg optionIdPrefix} \
+      ${lib.optionalString markdownByDefault "--markdown-by-default"} \
+      ${optionsJSON}/share/doc/nixos/options.json \
+      options.xml
+
+    if grep /nixpkgs/nixos/modules options.xml; then
       echo "The manual appears to depend on the location of Nixpkgs, which is bad"
       echo "since this prevents sharing via the NixOS channel.  This is typically"
       echo "caused by an option default that refers to a relative path (see above"
@@ -172,14 +176,7 @@ in rec {
       exit 1
     fi
 
-    ${pkgs.python3Minimal}/bin/python ${./sortXML.py} $optionsXML sorted.xml
-    ${pkgs.libxslt.bin}/bin/xsltproc \
-      --stringparam documentType '${documentType}' \
-      --stringparam revision '${revision}' \
-      --stringparam variablelistId '${variablelistId}' \
-      --stringparam optionIdPrefix '${optionIdPrefix}' \
-      -o intermediate.xml ${./options-to-docbook.xsl} sorted.xml
     ${pkgs.libxslt.bin}/bin/xsltproc \
-      -o "$out" ${./postprocess-option-descriptions.xsl} intermediate.xml
+      -o "$out" ${./postprocess-option-descriptions.xsl} options.xml
   '';
 }
diff --git a/nixos/lib/make-options-doc/generateDoc.py b/nixos/lib/make-options-doc/generateDoc.py
deleted file mode 100644
index 1fe4eb0253a..00000000000
--- a/nixos/lib/make-options-doc/generateDoc.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import argparse
-import json
-import sys
-
-formats = ['commonmark', 'asciidoc']
-
-parser = argparse.ArgumentParser(
-    description = 'Generate documentation for a set of JSON-formatted NixOS options'
-)
-parser.add_argument(
-    'nix_options_path',
-    help = 'a path to a JSON file containing the NixOS options'
-)
-parser.add_argument(
-    '-f',
-    '--format',
-    choices = formats,
-    required = True,
-    help = f'the documentation format to generate'
-)
-
-args = parser.parse_args()
-
-# Pretty-print certain Nix types, like literal expressions.
-def render_types(obj):
-    if '_type' not in obj: return obj
-
-    _type = obj['_type']
-    if _type == 'literalExpression' or _type == 'literalDocBook':
-        return obj['text']
-
-    if _type == 'derivation':
-        return obj['name']
-
-    raise Exception(f'Unexpected type `{_type}` in {json.dumps(obj)}')
-
-def generate_commonmark(options):
-    for (name, value) in options.items():
-        print('##', name.replace('<', '&lt;').replace('>', '&gt;'))
-        print(value['description'])
-        print()
-        if 'type' in value:
-            print('*_Type_*')
-            print ('```')
-            print(value['type'])
-            print ('```')
-        print()
-        print()
-        if 'default' in value:
-            print('*_Default_*')
-            print('```')
-            print(json.dumps(value['default'], ensure_ascii=False, separators=(',', ':')))
-            print('```')
-        print()
-        print()
-        if 'example' in value:
-            print('*_Example_*')
-            print('```')
-            print(json.dumps(value['example'], ensure_ascii=False, separators=(',', ':')))
-            print('```')
-        print()
-        print()
-
-# TODO: declarations: link to github
-def generate_asciidoc(options):
-    for (name, value) in options.items():
-        print(f'== {name}')
-        print()
-        print(value['description'])
-        print()
-        print('[discrete]')
-        print('=== details')
-        print()
-        print(f'Type:: {value["type"]}')
-        if 'default' in value:
-            print('Default::')
-            print('+')
-            print('----')
-            print(json.dumps(value['default'], ensure_ascii=False, separators=(',', ':')))
-            print('----')
-            print()
-        else:
-            print('No Default:: {blank}')
-        if value['readOnly']:
-            print('Read Only:: {blank}')
-        else:
-            print()
-        if 'example' in value:
-            print('Example::')
-            print('+')
-            print('----')
-            print(json.dumps(value['example'], ensure_ascii=False, separators=(',', ':')))
-            print('----')
-            print()
-        else:
-            print('No Example:: {blank}')
-        print()
-
-with open(args.nix_options_path) as nix_options_json:
-    options = json.load(nix_options_json, object_hook=render_types)
-
-    if args.format == 'commonmark':
-        generate_commonmark(options)
-    elif args.format == 'asciidoc':
-        generate_asciidoc(options)
-    else:
-        raise Exception(f'Unsupported documentation format `--format {args.format}`')
-
diff --git a/nixos/lib/make-options-doc/mergeJSON.py b/nixos/lib/make-options-doc/mergeJSON.py
index 750cd24fc65..b4f72b8a3fd 100644
--- a/nixos/lib/make-options-doc/mergeJSON.py
+++ b/nixos/lib/make-options-doc/mergeJSON.py
@@ -1,13 +1,9 @@
 import collections
 import json
+import os
 import sys
 from typing import Any, Dict, List
 
-# for MD conversion
-import mistune
-import re
-from xml.sax.saxutils import escape, quoteattr
-
 JSON = Dict[str, Any]
 
 class Key:
@@ -46,193 +42,20 @@ def unpivot(options: Dict[Key, Option]) -> Dict[str, JSON]:
         result[opt.name] = opt.value
     return result
 
-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):
-        tag = "link"
-        if link[0:1] == '#':
-            if text == "":
-                tag = "xref"
-            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"<{tag} {attr}={link}>{text}</{tag}>"
-    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 var(self, text):
-        return f"<varname>{escape(text)}</varname>"
-    def env(self, text):
-        return f"<envar>{escape(text)}</envar>"
-    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)
-
-def p_command(md):
-    COMMAND_PATTERN = r'\{command\}`(.*?)`'
-    def parse(self, m, state):
-        return ('command', m.group(1))
-    md.inline.register_rule('command', COMMAND_PATTERN, parse)
-    md.inline.rules.append('command')
-
-def p_file(md):
-    FILE_PATTERN = r'\{file\}`(.*?)`'
-    def parse(self, m, state):
-        return ('file', m.group(1))
-    md.inline.register_rule('file', FILE_PATTERN, parse)
-    md.inline.rules.append('file')
-
-def p_var(md):
-    VAR_PATTERN = r'\{var\}`(.*?)`'
-    def parse(self, m, state):
-        return ('var', m.group(1))
-    md.inline.register_rule('var', VAR_PATTERN, parse)
-    md.inline.rules.append('var')
-
-def p_env(md):
-    ENV_PATTERN = r'\{env\}`(.*?)`'
-    def parse(self, m, state):
-        return ('env', m.group(1))
-    md.inline.register_rule('env', ENV_PATTERN, parse)
-    md.inline.rules.append('env')
-
-def p_option(md):
-    OPTION_PATTERN = r'\{option\}`(.*?)`'
-    def parse(self, m, state):
-        return ('option', m.group(1))
-    md.inline.register_rule('option', OPTION_PATTERN, parse)
-    md.inline.rules.append('option')
-
-def p_manpage(md):
-    MANPAGE_PATTERN = r'\{manpage\}`(.*?)\((.+?)\)`'
-    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')
-
-def p_admonition(md):
-    ADMONITION_PATTERN = re.compile(r'^::: \{([^\n]*?)\}\n(.*?)^:::$\n*', flags=re.MULTILINE|re.DOTALL)
-    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')
-
-md = mistune.create_markdown(renderer=Renderer(), plugins=[
-    p_command, p_file, p_var, p_env, p_option, p_manpage, p_admonition
-])
-
-# converts in-place!
-def convertMD(options: Dict[str, Any]) -> str:
-    def convertString(path: str, text: str) -> str:
-        try:
-            rendered = md(text)
-            # keep trailing spaces so we can diff the generated XML to check for conversion bugs.
-            return rendered.rstrip() + text[len(text.rstrip()):]
-        except:
-            print(f"error in {path}")
-            raise
-
-    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():
-        try:
-            if optionIs(option, 'description', 'mdDoc'):
-                option['description'] = convertString(name, option['description']['text'])
-            elif markdownByDefault:
-                option['description'] = convertString(name, option['description'])
-
-            if optionIs(option, 'example', 'literalMD'):
-                docbook = convertString(name, option['example']['text'])
-                option['example'] = { '_type': 'literalDocBook', 'text': docbook }
-            if optionIs(option, 'default', 'literalMD'):
-                docbook = convertString(name, option['default']['text'])
-                option['default'] = { '_type': 'literalDocBook', 'text': docbook }
-        except Exception as e:
-            raise Exception(f"Failed to render option {name}: {str(e)}")
-
-
-    return options
-
 warningsAreErrors = False
+warnOnDocbook = False
 errorOnDocbook = False
-markdownByDefault = False
 optOffset = 0
 for arg in sys.argv[1:]:
     if arg == "--warnings-are-errors":
         optOffset += 1
         warningsAreErrors = True
-    if arg == "--error-on-docbook":
+    if arg == "--warn-on-docbook":
         optOffset += 1
-        errorOnDocbook = True
-    if arg == "--markdown-by-default":
+        warnOnDocbook = True
+    elif arg == "--error-on-docbook":
         optOffset += 1
-        markdownByDefault = True
+        errorOnDocbook = True
 
 options = pivot(json.load(open(sys.argv[1 + optOffset], 'r')))
 overrides = pivot(json.load(open(sys.argv[2 + optOffset], 'r')))
@@ -270,26 +93,27 @@ def is_docbook(o, key):
 # check that every option has a description
 hasWarnings = False
 hasErrors = False
-hasDocBookErrors = False
+hasDocBook = False
 for (k, v) in options.items():
-    if errorOnDocbook:
+    if warnOnDocbook or errorOnDocbook:
+        kind = "error" if errorOnDocbook else "warning"
         if isinstance(v.value.get('description', {}), str):
-            hasErrors = True
-            hasDocBookErrors = True
+            hasErrors |= errorOnDocbook
+            hasDocBook = True
             print(
-                f"\x1b[1;31merror: option {v.name} description uses DocBook\x1b[0m",
+                f"\x1b[1;31m{kind}: option {v.name} description uses DocBook\x1b[0m",
                 file=sys.stderr)
         elif is_docbook(v.value, 'defaultText'):
-            hasErrors = True
-            hasDocBookErrors = True
+            hasErrors |= errorOnDocbook
+            hasDocBook = True
             print(
-                f"\x1b[1;31merror: option {v.name} default uses DocBook\x1b[0m",
+                f"\x1b[1;31m{kind}: option {v.name} default uses DocBook\x1b[0m",
                 file=sys.stderr)
         elif is_docbook(v.value, 'example'):
-            hasErrors = True
-            hasDocBookErrors = True
+            hasErrors |= errorOnDocbook
+            hasDocBook = True
             print(
-                f"\x1b[1;31merror: option {v.name} example uses DocBook\x1b[0m",
+                f"\x1b[1;31m{kind}: option {v.name} example uses DocBook\x1b[0m",
                 file=sys.stderr)
 
     if v.value.get('description', None) is None:
@@ -302,19 +126,29 @@ for (k, v) in options.items():
             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 hasDocBookErrors:
+if hasDocBook:
+    (why, what) = (
+        ("disallowed for in-tree modules", "contribution") if errorOnDocbook
+        else ("deprecated for option documentation", "module")
+    )
     print("Explanation: The documentation contains descriptions, examples, or defaults written in DocBook. " +
         "NixOS is in the process of migrating from DocBook to Markdown, and " +
-        "DocBook is disallowed for in-tree modules. To change your contribution to "+
-        "use Markdown, apply mdDoc and literalMD. For example:\n" +
+        f"DocBook is {why}. To change your {what} to "+
+        "use Markdown, apply mdDoc and literalMD and use the *MD variants of option creation " +
+        "functions where they are available. For example:\n" +
         "\n" +
         "  example.foo = mkOption {\n" +
         "    description = lib.mdDoc ''your description'';\n" +
         "    defaultText = lib.literalMD ''your description of default'';\n" +
-        "  }\n" +
+        "  };\n" +
         "\n" +
-        "  example.enable = mkEnableOption (lib.mdDoc ''your thing'');",
+        "  example.enable = mkEnableOption (lib.mdDoc ''your thing'');\n" +
+        "  example.package = mkPackageOptionMD pkgs \"your-package\" {};\n" +
+        "  imports = [ (mkAliasOptionModuleMD [ \"example\" \"args\" ] [ \"example\" \"settings\" ]) ];",
         file = sys.stderr)
+    with open(os.getenv('TOUCH_IF_DB'), 'x'):
+        # just make sure it exists
+        pass
 
 if hasErrors:
     sys.exit(1)
@@ -327,4 +161,4 @@ if hasWarnings and warningsAreErrors:
         file=sys.stderr)
     sys.exit(1)
 
-json.dump(convertMD(unpivot(options)), fp=sys.stdout)
+json.dump(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
deleted file mode 100644
index ac49659c681..00000000000
--- a/nixos/lib/make-options-doc/options-to-docbook.xsl
+++ /dev/null
@@ -1,202 +0,0 @@
-<?xml version="1.0"?>
-
-<xsl:stylesheet version="1.0"
-                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
-                xmlns:str="http://exslt.org/strings"
-                xmlns:xlink="http://www.w3.org/1999/xlink"
-                xmlns:nixos="tag:nixos.org"
-                xmlns="http://docbook.org/ns/docbook"
-                extension-element-prefixes="str"
-                >
-
-  <xsl:output method='xml' encoding="UTF-8" />
-
-  <xsl:param name="revision" />
-  <xsl:param name="documentType" />
-  <xsl:param name="program" />
-  <xsl:param name="variablelistId" />
-  <xsl:param name="optionIdPrefix" />
-
-
-  <xsl:template match="/expr/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($optionIdPrefix,
-              translate(
-                attr[@name = 'name']/string/@value,
-                '*&lt; >[]:&quot;',
-                '________'
-            ))" />
-          <varlistentry>
-            <term xlink:href="#{$id}">
-              <xsl:attribute name="xml:id"><xsl:value-of select="$id"/></xsl:attribute>
-              <option>
-                <xsl:value-of select="attr[@name = 'name']/string/@value" />
-              </option>
-            </term>
-
-            <listitem>
-
-              <nixos:option-description>
-                <para>
-                  <xsl:value-of disable-output-escaping="yes"
-                                select="attr[@name = 'description']/string/@value" />
-                </para>
-              </nixos:option-description>
-
-              <xsl:if test="attr[@name = 'type']">
-                <para>
-                  <emphasis>Type:</emphasis>
-                  <xsl:text> </xsl:text>
-                  <xsl:value-of select="attr[@name = 'type']/string/@value"/>
-                  <xsl:if test="attr[@name = 'readOnly']/bool/@value = 'true'">
-                    <xsl:text> </xsl:text>
-                    <emphasis>(read only)</emphasis>
-                  </xsl:if>
-                </para>
-              </xsl:if>
-
-              <xsl:if test="attr[@name = 'default']">
-                <para>
-                  <emphasis>Default:</emphasis>
-                  <xsl:text> </xsl:text>
-                  <xsl:apply-templates select="attr[@name = 'default']/*" mode="top" />
-                </para>
-              </xsl:if>
-
-              <xsl:if test="attr[@name = 'example']">
-                <para>
-                  <emphasis>Example:</emphasis>
-                  <xsl:text> </xsl:text>
-                  <xsl:apply-templates select="attr[@name = 'example']/*" mode="top" />
-                </para>
-              </xsl:if>
-
-              <xsl:if test="attr[@name = 'relatedPackages']">
-                <para>
-                  <emphasis>Related packages:</emphasis>
-                  <xsl:text> </xsl:text>
-                  <xsl:value-of disable-output-escaping="yes"
-                                select="attr[@name = 'relatedPackages']/string/@value" />
-                </para>
-              </xsl:if>
-
-              <xsl:if test="count(attr[@name = 'declarations']/list/*) != 0">
-                <para>
-                  <emphasis>Declared by:</emphasis>
-                </para>
-                <xsl:apply-templates select="attr[@name = 'declarations']" />
-              </xsl:if>
-
-              <xsl:if test="count(attr[@name = 'definitions']/list/*) != 0">
-                <para>
-                  <emphasis>Defined by:</emphasis>
-                </para>
-                <xsl:apply-templates select="attr[@name = 'definitions']" />
-              </xsl:if>
-
-            </listitem>
-
-          </varlistentry>
-
-        </xsl:for-each>
-
-      </variablelist>
-  </xsl:template>
-
-
-  <xsl:template match="attrs[attr[@name = '_type' and string[@value = 'literalExpression']]]" mode = "top">
-    <xsl:choose>
-      <xsl:when test="contains(attr[@name = 'text']/string/@value, '&#010;')">
-        <programlisting><xsl:value-of select="attr[@name = 'text']/string/@value" /></programlisting>
-      </xsl:when>
-      <xsl:otherwise>
-        <literal><xsl:value-of select="attr[@name = 'text']/string/@value" /></literal>
-      </xsl:otherwise>
-    </xsl:choose>
-  </xsl:template>
-
-
-  <xsl:template match="attrs[attr[@name = '_type' and string[@value = 'literalDocBook']]]" mode = "top">
-    <xsl:value-of disable-output-escaping="yes" select="attr[@name = 'text']/string/@value" />
-  </xsl:template>
-
-
-  <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
-          repository (if it’s a module and we have a revision number),
-          or to the local filesystem. -->
-          <xsl:choose>
-            <xsl:when test="not(starts-with(@value, '/'))">
-              <xsl:choose>
-                <xsl:when test="$revision = 'local'">
-                  <xsl:attribute name="xlink:href">https://github.com/NixOS/nixpkgs/blob/master/<xsl:value-of select="@value"/></xsl:attribute>
-                </xsl:when>
-                <xsl:otherwise>
-                  <xsl:attribute name="xlink:href">https://github.com/NixOS/nixpkgs/blob/<xsl:value-of select="$revision"/>/<xsl:value-of select="@value"/></xsl:attribute>
-                </xsl:otherwise>
-              </xsl:choose>
-            </xsl:when>
-            <xsl:when test="$revision != 'local' and $program = 'nixops' and contains(@value, '/nix/')">
-              <xsl:attribute name="xlink:href">https://github.com/NixOS/nixops/blob/<xsl:value-of select="$revision"/>/nix/<xsl:value-of select="substring-after(@value, '/nix/')"/></xsl:attribute>
-            </xsl:when>
-            <xsl:otherwise>
-              <xsl:attribute name="xlink:href">file://<xsl:value-of select="@value"/></xsl:attribute>
-            </xsl:otherwise>
-          </xsl:choose>
-          <!-- Print the filename and make it user-friendly by replacing the
-          /nix/store/<hash> prefix by the default location of nixos
-          sources. -->
-          <xsl:choose>
-            <xsl:when test="not(starts-with(@value, '/'))">
-              &lt;nixpkgs/<xsl:value-of select="@value"/>&gt;
-            </xsl:when>
-            <xsl:when test="contains(@value, 'nixops') and contains(@value, '/nix/')">
-              &lt;nixops/<xsl:value-of select="substring-after(@value, '/nix/')"/>&gt;
-            </xsl:when>
-            <xsl:otherwise>
-              <xsl:value-of select="@value" />
-            </xsl:otherwise>
-          </xsl:choose>
-        </filename></member>
-      </xsl:for-each>
-    </simplelist>
-  </xsl:template>
-
-</xsl:stylesheet>
diff --git a/nixos/lib/make-options-doc/optionsJSONtoXML.nix b/nixos/lib/make-options-doc/optionsJSONtoXML.nix
deleted file mode 100644
index ba50c5f898b..00000000000
--- a/nixos/lib/make-options-doc/optionsJSONtoXML.nix
+++ /dev/null
@@ -1,6 +0,0 @@
-{ file }:
-
-builtins.attrValues
-  (builtins.mapAttrs
-    (name: def: def // { inherit name; })
-    (builtins.fromJSON (builtins.readFile file)))
diff --git a/nixos/lib/make-options-doc/sortXML.py b/nixos/lib/make-options-doc/sortXML.py
deleted file mode 100644
index e63ff3538b3..00000000000
--- a/nixos/lib/make-options-doc/sortXML.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import xml.etree.ElementTree as ET
-import sys
-
-tree = ET.parse(sys.argv[1])
-# the xml tree is of the form
-# <expr><list> {all options, each an attrs} </list></expr>
-options = list(tree.getroot().find('list'))
-
-def sortKey(opt):
-    def order(s):
-        if s.startswith("enable"):
-            return 0
-        if s.startswith("package"):
-            return 1
-        return 2
-
-    return [
-        (order(p.attrib['value']), p.attrib['value'])
-        for p in opt.findall('attr[@name="loc"]/list/string')
-    ]
-
-options.sort(key=sortKey)
-
-doc = ET.Element("expr")
-newOptions = ET.SubElement(doc, "list")
-newOptions.extend(options)
-ET.ElementTree(doc).write(sys.argv[2], encoding='utf-8')
diff --git a/nixos/lib/systemd-lib.nix b/nixos/lib/systemd-lib.nix
index c6c8753d532..a21450708fe 100644
--- a/nixos/lib/systemd-lib.nix
+++ b/nixos/lib/systemd-lib.nix
@@ -24,7 +24,7 @@ in rec {
         }
         ''
           name=${shellEscape name}
-          mkdir -p "$out/$(dirname "$name")"
+          mkdir -p "$out/$(dirname -- "$name")"
           echo -n "$text" > "$out/$name"
         ''
     else
diff --git a/nixos/lib/systemd-unit-options.nix b/nixos/lib/systemd-unit-options.nix
index 44f26572a23..6c53c5e0533 100644
--- a/nixos/lib/systemd-unit-options.nix
+++ b/nixos/lib/systemd-unit-options.nix
@@ -60,7 +60,7 @@ in rec {
         `asDropin` creates a drop-in file named `overrides.conf`.
         Mainly needed to define instances for systemd template units (e.g. `systemd-nspawn@mycontainer.service`).
 
-        See also systemd.unit(1).
+        See also {manpage}`systemd.unit(5)`.
       '';
     };
 
@@ -86,7 +86,7 @@ in rec {
 
         This option creates a `.wants` symlink in the given target that exists
         statelessly without the need for running `systemctl enable`.
-        The in systemd.unit(5) manpage described `[Install]` section however is
+        The `[Install]` section described in {manpage}`systemd.unit(5)` however is
         not supported because it is a stateful process that does not fit well
         into the NixOS design.
       '';
@@ -324,7 +324,11 @@ in rec {
       scriptArgs = mkOption {
         type = types.str;
         default = "";
-        description = lib.mdDoc "Arguments passed to the main process script.";
+        example = "%i";
+        description = lib.mdDoc ''
+          Arguments passed to the main process script.
+          Can contain specifiers (`%` placeholders expanded by systemd, see {manpage}`systemd.unit(5)`).
+        '';
       };
 
       preStart = mkOption {
diff --git a/nixos/lib/test-driver/default.nix b/nixos/lib/test-driver/default.nix
index e3786622c3c..33313059fff 100644
--- a/nixos/lib/test-driver/default.nix
+++ b/nixos/lib/test-driver/default.nix
@@ -31,7 +31,7 @@ python3Packages.buildPythonApplication rec {
     ++ extraPythonPackages python3Packages;
 
   doCheck = true;
-  checkInputs = with python3Packages; [ mypy pylint black ];
+  nativeCheckInputs = with python3Packages; [ mypy pylint black ];
   checkPhase = ''
     mypy --disallow-untyped-defs \
           --no-implicit-optional \
diff --git a/nixos/lib/test-driver/test_driver/__init__.py b/nixos/lib/test-driver/test_driver/__init__.py
index 61d91c9ed65..db7e0ed33a8 100755
--- a/nixos/lib/test-driver/test_driver/__init__.py
+++ b/nixos/lib/test-driver/test_driver/__init__.py
@@ -41,11 +41,9 @@ def writeable_dir(arg: str) -> Path:
     """
     path = Path(arg)
     if not path.is_dir():
-        raise argparse.ArgumentTypeError("{0} is not a directory".format(path))
+        raise argparse.ArgumentTypeError(f"{path} is not a directory")
     if not os.access(path, os.W_OK):
-        raise argparse.ArgumentTypeError(
-            "{0} is not a writeable directory".format(path)
-        )
+        raise argparse.ArgumentTypeError(f"{path} is not a writeable directory")
     return path
 
 
diff --git a/nixos/lib/test-driver/test_driver/driver.py b/nixos/lib/test-driver/test_driver/driver.py
index e32f6810ca8..ea6ba4b65b5 100644
--- a/nixos/lib/test-driver/test_driver/driver.py
+++ b/nixos/lib/test-driver/test_driver/driver.py
@@ -2,6 +2,7 @@ from contextlib import contextmanager
 from pathlib import Path
 from typing import Any, Dict, Iterator, List, Union, Optional, Callable, ContextManager
 import os
+import re
 import tempfile
 
 from test_driver.logger import rootlog
@@ -19,19 +20,19 @@ def get_tmp_dir() -> Path:
     tmp_dir.mkdir(mode=0o700, exist_ok=True)
     if not tmp_dir.is_dir():
         raise NotADirectoryError(
-            "The directory defined by TMPDIR, TEMP, TMP or CWD: {0} is not a directory".format(
-                tmp_dir
-            )
+            f"The directory defined by TMPDIR, TEMP, TMP or CWD: {tmp_dir} is not a directory"
         )
     if not os.access(tmp_dir, os.W_OK):
         raise PermissionError(
-            "The directory defined by TMPDIR, TEMP, TMP, or CWD: {0} is not writeable".format(
-                tmp_dir
-            )
+            f"The directory defined by TMPDIR, TEMP, TMP, or CWD: {tmp_dir} is not writeable"
         )
     return tmp_dir
 
 
+def pythonize_name(name: str) -> str:
+    return re.sub(r"^[^A-z_]|[^A-z0-9_]", "_", name)
+
+
 class Driver:
     """A handle to the driver that sets up the environment
     and runs the tests"""
@@ -117,7 +118,7 @@ class Driver:
             polling_condition=self.polling_condition,
             Machine=Machine,  # for typing
         )
-        machine_symbols = {m.name: m for m in self.machines}
+        machine_symbols = {pythonize_name(m.name): m for m in self.machines}
         # If there's exactly one machine, make it available under the name
         # "machine", even if it's not called that.
         if len(self.machines) == 1:
@@ -183,7 +184,6 @@ class Driver:
             start_command=cmd,
             name=name,
             keep_vm_state=args.get("keep_vm_state", False),
-            allow_reboot=args.get("allow_reboot", False),
         )
 
     def serial_stdout_on(self) -> None:
@@ -220,6 +220,20 @@ class Driver:
                 res = driver.polling_conditions.pop()
                 assert res is self.condition
 
+            def wait(self, timeout: int = 900) -> None:
+                def condition(last: bool) -> bool:
+                    if last:
+                        rootlog.info(f"Last chance for {self.condition.description}")
+                    ret = self.condition.check(force=True)
+                    if not ret and not last:
+                        rootlog.info(
+                            f"({self.condition.description} failure not fatal yet)"
+                        )
+                    return ret
+
+                with rootlog.nested(f"waiting for {self.condition.description}"):
+                    retry(condition, timeout=timeout)
+
         if fun_ is None:
             return Poll
         else:
diff --git a/nixos/lib/test-driver/test_driver/logger.py b/nixos/lib/test-driver/test_driver/logger.py
index 59ed2954723..e6182ff7c76 100644
--- a/nixos/lib/test-driver/test_driver/logger.py
+++ b/nixos/lib/test-driver/test_driver/logger.py
@@ -36,7 +36,7 @@ class Logger:
 
     def maybe_prefix(self, message: str, attributes: Dict[str, str]) -> str:
         if "machine" in attributes:
-            return "{}: {}".format(attributes["machine"], message)
+            return f"{attributes['machine']}: {message}"
         return message
 
     def log_line(self, message: str, attributes: Dict[str, str]) -> None:
@@ -62,9 +62,7 @@ class Logger:
     def log_serial(self, message: str, machine: str) -> None:
         self.enqueue({"msg": message, "machine": machine, "type": "serial"})
         if self._print_serial_logs:
-            self._eprint(
-                Style.DIM + "{} # {}".format(machine, message) + Style.RESET_ALL
-            )
+            self._eprint(Style.DIM + f"{machine} # {message}" + Style.RESET_ALL)
 
     def enqueue(self, item: Dict[str, str]) -> None:
         self.queue.put(item)
@@ -97,7 +95,7 @@ class Logger:
         yield
         self.drain_log_queue()
         toc = time.time()
-        self.log("(finished: {}, in {:.2f} seconds)".format(message, toc - tic))
+        self.log(f"(finished: {message}, in {toc - tic:.2f} seconds)")
 
         self.xml.endElement("nest")
 
diff --git a/nixos/lib/test-driver/test_driver/machine.py b/nixos/lib/test-driver/test_driver/machine.py
index ffbc7c18e42..9de98c217a5 100644
--- a/nixos/lib/test-driver/test_driver/machine.py
+++ b/nixos/lib/test-driver/test_driver/machine.py
@@ -1,4 +1,4 @@
-from contextlib import _GeneratorContextManager
+from contextlib import _GeneratorContextManager, nullcontext
 from pathlib import Path
 from queue import Queue
 from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
@@ -101,14 +101,14 @@ def _perform_ocr_on_screenshot(
 
     tess_args = f"-c debug_file=/dev/null --psm 11"
 
-    cmd = f"convert {magick_args} {screenshot_path} tiff:{screenshot_path}.tiff"
+    cmd = f"convert {magick_args} '{screenshot_path}' 'tiff:{screenshot_path}.tiff'"
     ret = subprocess.run(cmd, shell=True, capture_output=True)
     if ret.returncode != 0:
         raise Exception(f"TIFF conversion failed with exit code {ret.returncode}")
 
     model_results = []
     for model_id in model_ids:
-        cmd = f"tesseract {screenshot_path}.tiff - {tess_args} --oem {model_id}"
+        cmd = f"tesseract '{screenshot_path}.tiff' - {tess_args} --oem '{model_id}'"
         ret = subprocess.run(cmd, shell=True, capture_output=True)
         if ret.returncode != 0:
             raise Exception(f"OCR failed with exit code {ret.returncode}")
@@ -144,7 +144,7 @@ class StartCommand:
         self,
         monitor_socket_path: Path,
         shell_socket_path: Path,
-        allow_reboot: bool = False,  # TODO: unused, legacy?
+        allow_reboot: bool = False,
     ) -> str:
         display_opts = ""
         display_available = any(x in os.environ for x in ["DISPLAY", "WAYLAND_DISPLAY"])
@@ -152,16 +152,14 @@ class StartCommand:
             display_opts += " -nographic"
 
         # qemu options
-        qemu_opts = ""
-        qemu_opts += (
-            ""
-            if allow_reboot
-            else " -no-reboot"
+        qemu_opts = (
             " -device virtio-serial"
             " -device virtconsole,chardev=shell"
             " -device virtio-rng-pci"
             " -serial stdio"
         )
+        if not allow_reboot:
+            qemu_opts += " -no-reboot"
         # TODO: qemu script already catpures this env variable, legacy?
         qemu_opts += " " + os.environ.get("QEMU_OPTS", "")
 
@@ -195,9 +193,10 @@ class StartCommand:
         shared_dir: Path,
         monitor_socket_path: Path,
         shell_socket_path: Path,
+        allow_reboot: bool,
     ) -> subprocess.Popen:
         return subprocess.Popen(
-            self.cmd(monitor_socket_path, shell_socket_path),
+            self.cmd(monitor_socket_path, shell_socket_path, allow_reboot),
             stdin=subprocess.PIPE,
             stdout=subprocess.PIPE,
             stderr=subprocess.STDOUT,
@@ -312,7 +311,6 @@ class Machine:
 
     start_command: StartCommand
     keep_vm_state: bool
-    allow_reboot: bool
 
     process: Optional[subprocess.Popen]
     pid: Optional[int]
@@ -337,13 +335,11 @@ class Machine:
         start_command: StartCommand,
         name: str = "machine",
         keep_vm_state: bool = False,
-        allow_reboot: bool = False,
         callbacks: Optional[List[Callable]] = None,
     ) -> None:
         self.out_dir = out_dir
         self.tmp_dir = tmp_dir
         self.keep_vm_state = keep_vm_state
-        self.allow_reboot = allow_reboot
         self.name = name
         self.start_command = start_command
         self.callbacks = callbacks if callbacks is not None else []
@@ -406,25 +402,23 @@ class Machine:
         return rootlog.nested(msg, my_attrs)
 
     def wait_for_monitor_prompt(self) -> str:
-        with self.nested("waiting for monitor prompt"):
-            assert self.monitor is not None
-            answer = ""
-            while True:
-                undecoded_answer = self.monitor.recv(1024)
-                if not undecoded_answer:
-                    break
-                answer += undecoded_answer.decode()
-                if answer.endswith("(qemu) "):
-                    break
-            return answer
+        assert self.monitor is not None
+        answer = ""
+        while True:
+            undecoded_answer = self.monitor.recv(1024)
+            if not undecoded_answer:
+                break
+            answer += undecoded_answer.decode()
+            if answer.endswith("(qemu) "):
+                break
+        return answer
 
     def send_monitor_command(self, command: str) -> str:
         self.run_callbacks()
-        with self.nested("sending monitor command: {}".format(command)):
-            message = ("{}\n".format(command)).encode()
-            assert self.monitor is not None
-            self.monitor.send(message)
-            return self.wait_for_monitor_prompt()
+        message = f"{command}\n".encode()
+        assert self.monitor is not None
+        self.monitor.send(message)
+        return self.wait_for_monitor_prompt()
 
     def wait_for_unit(
         self, unit: str, user: Optional[str] = None, timeout: int = 900
@@ -438,7 +432,7 @@ class Machine:
             info = self.get_unit_info(unit, user)
             state = info["ActiveState"]
             if state == "failed":
-                raise Exception('unit "{}" reached state "{}"'.format(unit, state))
+                raise Exception(f'unit "{unit}" reached state "{state}"')
 
             if state == "inactive":
                 status, jobs = self.systemctl("list-jobs --full 2>&1", user)
@@ -446,27 +440,24 @@ class Machine:
                     info = self.get_unit_info(unit, user)
                     if info["ActiveState"] == state:
                         raise Exception(
-                            (
-                                'unit "{}" is inactive and there ' "are no pending jobs"
-                            ).format(unit)
+                            f'unit "{unit}" is inactive and there are no pending jobs'
                         )
 
             return state == "active"
 
         with self.nested(
-            "waiting for unit {}{}".format(
-                unit, f" with user {user}" if user is not None else ""
-            )
+            f"waiting for unit {unit}"
+            + (f" with user {user}" if user is not None else "")
         ):
             retry(check_active, timeout)
 
     def get_unit_info(self, unit: str, user: Optional[str] = None) -> Dict[str, str]:
-        status, lines = self.systemctl('--no-pager show "{}"'.format(unit), user)
+        status, lines = self.systemctl(f'--no-pager show "{unit}"', user)
         if status != 0:
             raise Exception(
-                'retrieving systemctl info for unit "{}" {} failed with exit code {}'.format(
-                    unit, "" if user is None else 'under user "{}"'.format(user), status
-                )
+                f'retrieving systemctl info for unit "{unit}"'
+                + ("" if user is None else f' under user "{user}"')
+                + f" failed with exit code {status}"
             )
 
         line_pattern = re.compile(r"^([^=]+)=(.*)$")
@@ -486,24 +477,22 @@ class Machine:
         if user is not None:
             q = q.replace("'", "\\'")
             return self.execute(
-                (
-                    "su -l {} --shell /bin/sh -c "
-                    "$'XDG_RUNTIME_DIR=/run/user/`id -u` "
-                    "systemctl --user {}'"
-                ).format(user, q)
+                f"su -l {user} --shell /bin/sh -c "
+                "$'XDG_RUNTIME_DIR=/run/user/`id -u` "
+                f"systemctl --user {q}'"
             )
-        return self.execute("systemctl {}".format(q))
+        return self.execute(f"systemctl {q}")
 
     def require_unit_state(self, unit: str, require_state: str = "active") -> None:
         with self.nested(
-            "checking if unit ‘{}’ has reached state '{}'".format(unit, require_state)
+            f"checking if unit '{unit}' has reached state '{require_state}'"
         ):
             info = self.get_unit_info(unit)
             state = info["ActiveState"]
             if state != require_state:
                 raise Exception(
-                    "Expected unit ‘{}’ to to be in state ".format(unit)
-                    + "'{}' but it is in state ‘{}’".format(require_state, state)
+                    f"Expected unit '{unit}' to to be in state "
+                    f"'{require_state}' but it is in state '{state}'"
                 )
 
     def _next_newline_closed_block_from_shell(self) -> str:
@@ -552,20 +541,29 @@ class Machine:
         self.shell.send("echo ${PIPESTATUS[0]}\n".encode())
         rc = int(self._next_newline_closed_block_from_shell().strip())
 
-        return (rc, output.decode())
+        return (rc, output.decode(errors="replace"))
 
-    def shell_interact(self) -> None:
-        """Allows you to interact with the guest shell
+    def shell_interact(self, address: Optional[str] = None) -> None:
+        """Allows you to interact with the guest shell for debugging purposes.
 
-        Should only be used during test development, not in the production test."""
+        @address string passed to socat that will be connected to the guest shell.
+        Check the `Running Tests interactivly` chapter of NixOS manual for an example.
+        """
         self.connect()
-        self.log("Terminal is ready (there is no initial prompt):")
+
+        if address is None:
+            address = "READLINE,prompt=$ "
+            self.log("Terminal is ready (there is no initial prompt):")
 
         assert self.shell
-        subprocess.run(
-            ["socat", "READLINE,prompt=$ ", f"FD:{self.shell.fileno()}"],
-            pass_fds=[self.shell.fileno()],
-        )
+        try:
+            subprocess.run(
+                ["socat", address, f"FD:{self.shell.fileno()}"],
+                pass_fds=[self.shell.fileno()],
+            )
+            # allow users to cancel this command without breaking the test
+        except KeyboardInterrupt:
+            pass
 
     def console_interact(self) -> None:
         """Allows you to interact with QEMU's stdin
@@ -593,13 +591,11 @@ class Machine:
         """Execute each command and check that it succeeds."""
         output = ""
         for command in commands:
-            with self.nested("must succeed: {}".format(command)):
+            with self.nested(f"must succeed: {command}"):
                 (status, out) = self.execute(command, timeout=timeout)
                 if status != 0:
-                    self.log("output: {}".format(out))
-                    raise Exception(
-                        "command `{}` failed (exit code {})".format(command, status)
-                    )
+                    self.log(f"output: {out}")
+                    raise Exception(f"command `{command}` failed (exit code {status})")
                 output += out
         return output
 
@@ -607,12 +603,10 @@ class Machine:
         """Execute each command and check that it fails."""
         output = ""
         for command in commands:
-            with self.nested("must fail: {}".format(command)):
+            with self.nested(f"must fail: {command}"):
                 (status, out) = self.execute(command, timeout=timeout)
                 if status == 0:
-                    raise Exception(
-                        "command `{}` unexpectedly succeeded".format(command)
-                    )
+                    raise Exception(f"command `{command}` unexpectedly succeeded")
                 output += out
         return output
 
@@ -627,7 +621,7 @@ class Machine:
             status, output = self.execute(command, timeout=timeout)
             return status == 0
 
-        with self.nested("waiting for success: {}".format(command)):
+        with self.nested(f"waiting for success: {command}"):
             retry(check_success, timeout)
             return output
 
@@ -642,7 +636,7 @@ class Machine:
             status, output = self.execute(command, timeout=timeout)
             return status != 0
 
-        with self.nested("waiting for failure: {}".format(command)):
+        with self.nested(f"waiting for failure: {command}"):
             retry(check_failure)
             return output
 
@@ -661,8 +655,8 @@ class Machine:
 
     def get_tty_text(self, tty: str) -> str:
         status, output = self.execute(
-            "fold -w$(stty -F /dev/tty{0} size | "
-            "awk '{{print $2}}') /dev/vcs{0}".format(tty)
+            f"fold -w$(stty -F /dev/tty{tty} size | "
+            f"awk '{{print $2}}') /dev/vcs{tty}"
         )
         return output
 
@@ -681,45 +675,45 @@ class Machine:
                 )
             return len(matcher.findall(text)) > 0
 
-        with self.nested("waiting for {} to appear on tty {}".format(regexp, tty)):
+        with self.nested(f"waiting for {regexp} to appear on tty {tty}"):
             retry(tty_matches)
 
     def send_chars(self, chars: str, delay: Optional[float] = 0.01) -> None:
-        with self.nested("sending keys ‘{}‘".format(chars)):
+        with self.nested(f"sending keys {repr(chars)}"):
             for char in chars:
-                self.send_key(char, delay)
+                self.send_key(char, delay, log=False)
 
     def wait_for_file(self, filename: str) -> None:
         """Waits until the file exists in machine's file system."""
 
         def check_file(_: Any) -> bool:
-            status, _ = self.execute("test -e {}".format(filename))
+            status, _ = self.execute(f"test -e {filename}")
             return status == 0
 
-        with self.nested("waiting for file ‘{}‘".format(filename)):
+        with self.nested(f"waiting for file '{filename}'"):
             retry(check_file)
 
-    def wait_for_open_port(self, port: int) -> None:
+    def wait_for_open_port(self, port: int, addr: str = "localhost") -> None:
         def port_is_open(_: Any) -> bool:
-            status, _ = self.execute("nc -z localhost {}".format(port))
+            status, _ = self.execute(f"nc -z {addr} {port}")
             return status == 0
 
-        with self.nested("waiting for TCP port {}".format(port)):
+        with self.nested(f"waiting for TCP port {port} on {addr}"):
             retry(port_is_open)
 
-    def wait_for_closed_port(self, port: int) -> None:
+    def wait_for_closed_port(self, port: int, addr: str = "localhost") -> None:
         def port_is_closed(_: Any) -> bool:
-            status, _ = self.execute("nc -z localhost {}".format(port))
+            status, _ = self.execute(f"nc -z {addr} {port}")
             return status != 0
 
-        with self.nested("waiting for TCP port {} to be closed".format(port)):
+        with self.nested(f"waiting for TCP port {port} on {addr} to be closed"):
             retry(port_is_closed)
 
     def start_job(self, jobname: str, user: Optional[str] = None) -> Tuple[int, str]:
-        return self.systemctl("start {}".format(jobname), user)
+        return self.systemctl(f"start {jobname}", user)
 
     def stop_job(self, jobname: str, user: Optional[str] = None) -> Tuple[int, str]:
-        return self.systemctl("stop {}".format(jobname), user)
+        return self.systemctl(f"stop {jobname}", user)
 
     def wait_for_job(self, jobname: str) -> None:
         self.wait_for_unit(jobname)
@@ -739,21 +733,22 @@ class Machine:
             toc = time.time()
 
             self.log("connected to guest root shell")
-            self.log("(connecting took {:.2f} seconds)".format(toc - tic))
+            self.log(f"(connecting took {toc - tic:.2f} seconds)")
             self.connected = True
 
     def screenshot(self, filename: str) -> None:
-        word_pattern = re.compile(r"^\w+$")
-        if word_pattern.match(filename):
-            filename = os.path.join(self.out_dir, "{}.png".format(filename))
-        tmp = "{}.ppm".format(filename)
+        if "." not in filename:
+            filename += ".png"
+        if "/" not in filename:
+            filename = os.path.join(self.out_dir, filename)
+        tmp = f"{filename}.ppm"
 
         with self.nested(
-            "making screenshot {}".format(filename),
+            f"making screenshot {filename}",
             {"image": os.path.basename(filename)},
         ):
-            self.send_monitor_command("screendump {}".format(tmp))
-            ret = subprocess.run("pnmtopng {} > {}".format(tmp, filename), shell=True)
+            self.send_monitor_command(f"screendump {tmp}")
+            ret = subprocess.run(f"pnmtopng '{tmp}' > '{filename}'", shell=True)
             os.unlink(tmp)
             if ret.returncode != 0:
                 raise Exception("Cannot convert screenshot")
@@ -815,7 +810,7 @@ class Machine:
 
     def dump_tty_contents(self, tty: str) -> None:
         """Debugging: Dump the contents of the TTY<n>"""
-        self.execute("fold -w 80 /dev/vcs{} | systemd-cat".format(tty))
+        self.execute(f"fold -w 80 /dev/vcs{tty} | systemd-cat")
 
     def _get_screen_text_variants(self, model_ids: Iterable[int]) -> List[str]:
         with tempfile.TemporaryDirectory() as tmpdir:
@@ -837,15 +832,15 @@ class Machine:
                     return True
 
             if last:
-                self.log("Last OCR attempt failed. Text was: {}".format(variants))
+                self.log(f"Last OCR attempt failed. Text was: {variants}")
 
             return False
 
-        with self.nested("waiting for {} to appear on screen".format(regex)):
+        with self.nested(f"waiting for {regex} to appear on screen"):
             retry(screen_matches)
 
     def wait_for_console_text(self, regex: str) -> None:
-        with self.nested("waiting for {} to appear on console".format(regex)):
+        with self.nested(f"waiting for {regex} to appear on console"):
             # Buffer the console output, this is needed
             # to match multiline regexes.
             console = io.StringIO()
@@ -860,11 +855,15 @@ class Machine:
                 if matches is not None:
                     return
 
-    def send_key(self, key: str, delay: Optional[float] = 0.01) -> None:
+    def send_key(
+        self, key: str, delay: Optional[float] = 0.01, log: Optional[bool] = True
+    ) -> None:
         key = CHAR_TO_KEY.get(key, key)
-        self.send_monitor_command("sendkey {}".format(key))
-        if delay is not None:
-            time.sleep(delay)
+        context = self.nested(f"sending key {repr(key)}") if log else nullcontext()
+        with context:
+            self.send_monitor_command(f"sendkey {key}")
+            if delay is not None:
+                time.sleep(delay)
 
     def send_console(self, chars: str) -> None:
         assert self.process
@@ -872,7 +871,7 @@ class Machine:
         self.process.stdin.write(chars.encode())
         self.process.stdin.flush()
 
-    def start(self) -> None:
+    def start(self, allow_reboot: bool = False) -> None:
         if self.booted:
             return
 
@@ -896,6 +895,7 @@ class Machine:
             self.shared_dir,
             self.monitor_path,
             self.shell_path,
+            allow_reboot,
         )
         self.monitor, _ = monitor_socket.accept()
         self.shell, _ = shell_socket.accept()
@@ -921,7 +921,7 @@ class Machine:
         self.pid = self.process.pid
         self.booted = True
 
-        self.log("QEMU running (pid {})".format(self.pid))
+        self.log(f"QEMU running (pid {self.pid})")
 
     def cleanup_statedir(self) -> None:
         shutil.rmtree(self.state_dir)
@@ -944,6 +944,15 @@ class Machine:
         self.send_monitor_command("quit")
         self.wait_for_shutdown()
 
+    def reboot(self) -> None:
+        """Press Ctrl+Alt+Delete in the guest.
+
+        Prepares the machine to be reconnected which is useful if the
+        machine was started with `allow_reboot = True`
+        """
+        self.send_key(f"ctrl-alt-delete")
+        self.connected = False
+
     def wait_for_x(self) -> None:
         """Wait until it is possible to connect to the X server.  Note that
         testing the existence of /tmp/.X11-unix/X0 is insufficient.
@@ -975,7 +984,7 @@ class Machine:
             names = self.get_window_names()
             if last_try:
                 self.log(
-                    "Last chance to match {} on the window list,".format(regexp)
+                    f"Last chance to match {regexp} on the window list,"
                     + " which currently contains: "
                     + ", ".join(names)
                 )
@@ -992,9 +1001,7 @@ class Machine:
         """Forward a TCP port on the host to a TCP port on the guest.
         Useful during interactive testing.
         """
-        self.send_monitor_command(
-            "hostfwd_add tcp::{}-:{}".format(host_port, guest_port)
-        )
+        self.send_monitor_command(f"hostfwd_add tcp::{host_port}-:{guest_port}")
 
     def block(self) -> None:
         """Make the machine unreachable by shutting down eth1 (the multicast
diff --git a/nixos/lib/test-driver/test_driver/polling_condition.py b/nixos/lib/test-driver/test_driver/polling_condition.py
index 459845452fa..02ca0a03ab3 100644
--- a/nixos/lib/test-driver/test_driver/polling_condition.py
+++ b/nixos/lib/test-driver/test_driver/polling_condition.py
@@ -1,4 +1,5 @@
 from typing import Callable, Optional
+from math import isfinite
 import time
 
 from .logger import rootlog
@@ -14,7 +15,7 @@ class PollingCondition:
     description: Optional[str]
 
     last_called: float
-    entered: bool
+    entry_count: int
 
     def __init__(
         self,
@@ -34,14 +35,21 @@ class PollingCondition:
             self.description = str(description)
 
         self.last_called = float("-inf")
-        self.entered = False
+        self.entry_count = 0
 
-    def check(self) -> bool:
-        if self.entered or not self.overdue:
+    def check(self, force: bool = False) -> bool:
+        if (self.entered or not self.overdue) and not force:
             return True
 
         with self, rootlog.nested(self.nested_message):
-            rootlog.info(f"Time since last: {time.monotonic() - self.last_called:.2f}s")
+            time_since_last = time.monotonic() - self.last_called
+            last_message = (
+                f"Time since last: {time_since_last:.2f}s"
+                if isfinite(time_since_last)
+                else "(not called yet)"
+            )
+
+            rootlog.info(last_message)
             try:
                 res = self.condition()  # type: ignore
             except Exception:
@@ -69,9 +77,16 @@ class PollingCondition:
     def overdue(self) -> bool:
         return self.last_called + self.seconds_interval < time.monotonic()
 
+    @property
+    def entered(self) -> bool:
+        # entry_count should never dip *below* zero
+        assert self.entry_count >= 0
+        return self.entry_count > 0
+
     def __enter__(self) -> None:
-        self.entered = True
+        self.entry_count += 1
 
     def __exit__(self, exc_type, exc_value, traceback) -> None:  # type: ignore
-        self.entered = False
+        assert self.entered
+        self.entry_count -= 1
         self.last_called = time.monotonic()
diff --git a/nixos/lib/testing-python.nix b/nixos/lib/testing-python.nix
index 134d38f1b67..4904ad6e359 100644
--- a/nixos/lib/testing-python.nix
+++ b/nixos/lib/testing-python.nix
@@ -1,3 +1,4 @@
+args@
 { system
 , pkgs ? import ../.. { inherit system config; }
   # Use a minimal kernel?
@@ -5,7 +6,7 @@
   # Ignored
 , config ? { }
   # !!! See comment about args in lib/modules.nix
-, specialArgs ? { }
+, specialArgs ? throw "legacy - do not use, see error below"
   # Modules to add to each VM
 , extraConfigurations ? [ ]
 }:
@@ -13,6 +14,13 @@ let
   nixos-lib = import ./default.nix { inherit (pkgs) lib; };
 in
 
+pkgs.lib.throwIf (args?specialArgs) ''
+  testing-python.nix: `specialArgs` is not supported anymore. If you're looking
+  for the public interface to the NixOS test framework, use `runTest`, and
+  `node.specialArgs`.
+  See https://nixos.org/manual/nixos/unstable/index.html#sec-calling-nixos-tests
+  and https://nixos.org/manual/nixos/unstable/index.html#test-opt-node.specialArgs
+''
 rec {
 
   inherit pkgs;
diff --git a/nixos/lib/testing/default.nix b/nixos/lib/testing/default.nix
index 9d4f9dbc43d..a89f734b1e6 100644
--- a/nixos/lib/testing/default.nix
+++ b/nixos/lib/testing/default.nix
@@ -1,7 +1,10 @@
 { lib }:
 let
 
-  evalTest = module: lib.evalModules { modules = testModules ++ [ module ]; };
+  evalTest = module: lib.evalModules {
+    modules = testModules ++ [ module ];
+    class = "nixosTest";
+  };
   runTest = module: (evalTest ({ config, ... }: { imports = [ module ]; result = config.test; })).config.result;
 
   testModules = [
diff --git a/nixos/lib/testing/driver.nix b/nixos/lib/testing/driver.nix
index fb181c1d7e9..25759a91dda 100644
--- a/nixos/lib/testing/driver.nix
+++ b/nixos/lib/testing/driver.nix
@@ -21,29 +21,20 @@ let
     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
-  # beyond RFC1035
-  invalidNodeNames = lib.filter
-    (node: builtins.match "^[A-z_]([A-z0-9_]+)?$" node == null)
-    nodeHostNames;
+  pythonizeName = name:
+    let
+      head = lib.substring 0 1 name;
+      tail = lib.substring 1 (-1) name;
+    in
+      (if builtins.match "[A-z_]" head == null then "_" else head) +
+      lib.stringAsChars (c: if builtins.match "[A-z0-9_]" c == null then "_" else c) tail;
 
   uniqueVlans = lib.unique (builtins.concatLists vlans);
   vlanNames = map (i: "vlan${toString i}: VLan;") uniqueVlans;
-  machineNames = map (name: "${name}: Machine;") nodeHostNames;
+  pythonizedNames = map pythonizeName nodeHostNames;
+  machineNames = map (name: "${name}: Machine;") pythonizedNames;
 
-  withChecks =
-    if lib.length invalidNodeNames > 0 then
-      throw ''
-        Cannot create machines out of (${lib.concatStringsSep ", " invalidNodeNames})!
-        All machines are referenced as python variables in the testing framework which will break the
-        script when special characters are used.
-
-        This is an IMPLEMENTATION ERROR and needs to be fixed. Meanwhile,
-        please stick to alphanumeric chars and underscores as separation.
-      ''
-    else
-      lib.warnIf config.skipLint "Linting is disabled";
+  withChecks = lib.warnIf config.skipLint "Linting is disabled";
 
   driver =
     hostPkgs.runCommand "nixos-test-driver-${config.name}"
@@ -87,7 +78,7 @@ let
         ${testDriver}/bin/generate-driver-symbols
         ${lib.optionalString (!config.skipLint) ''
           PYFLAKES_BUILTINS="$(
-            echo -n ${lib.escapeShellArg (lib.concatStringsSep "," nodeHostNames)},
+            echo -n ${lib.escapeShellArg (lib.concatStringsSep "," pythonizedNames)},
             < ${lib.escapeShellArg "driver-symbols"}
           )" ${hostPkgs.python3Packages.pyflakes}/bin/pyflakes $out/test-script
         ''}
diff --git a/nixos/lib/testing/legacy.nix b/nixos/lib/testing/legacy.nix
index 868b8b65b17..b3105755660 100644
--- a/nixos/lib/testing/legacy.nix
+++ b/nixos/lib/testing/legacy.nix
@@ -3,9 +3,10 @@ let
   inherit (lib) mkIf mkOption types;
 in
 {
-  # This needs options.warnings, which we don't have (yet?).
+  # This needs options.warnings and options.assertions, which we don't have (yet?).
   # imports = [
   #   (lib.mkRenamedOptionModule [ "machine" ] [ "nodes" "machine" ])
+  #   (lib.mkRemovedOptionModule [ "minimal" ] "The minimal kernel module was removed as it was broken and not used any more in nixpkgs.")
   # ];
 
   options = {
diff --git a/nixos/lib/testing/meta.nix b/nixos/lib/testing/meta.nix
index 65754fe3c54..805b7520edf 100644
--- a/nixos/lib/testing/meta.nix
+++ b/nixos/lib/testing/meta.nix
@@ -22,7 +22,7 @@ in
           };
           timeout = lib.mkOption {
             type = types.nullOr types.int;
-            default = null; # NOTE: null values are filtered out by `meta`.
+            default = 3600;  # 1 hour
             description = mdDoc ''
               The [{option}`test`](#test-opt-test)'s [`meta.timeout`](https://nixos.org/manual/nixpkgs/stable/#var-meta-timeout) in seconds.
             '';
diff --git a/nixos/lib/testing/nodes.nix b/nixos/lib/testing/nodes.nix
index 8e620c96b3b..c538ab468c5 100644
--- a/nixos/lib/testing/nodes.nix
+++ b/nixos/lib/testing/nodes.nix
@@ -23,7 +23,7 @@ let
               nixpkgs.config.allowAliases = false;
             })
           testModuleArgs.config.extraBaseModules
-        ] ++ optional config.minimal ../../modules/testing/minimal-kernel.nix;
+        ];
     };
 
 
@@ -78,14 +78,6 @@ in
       '';
     };
 
-    minimal = mkOption {
-      type = types.bool;
-      default = false;
-      description = mdDoc ''
-        Enable to configure all [{option}`nodes`](#test-opt-nodes) to run with a minimal kernel.
-      '';
-    };
-
     nodesCompat = mkOption {
       internal = true;
       description = mdDoc ''
diff --git a/nixos/lib/testing/testScript.nix b/nixos/lib/testing/testScript.nix
index 5d4181c5f5d..5c36d754d79 100644
--- a/nixos/lib/testing/testScript.nix
+++ b/nixos/lib/testing/testScript.nix
@@ -7,7 +7,7 @@ in
   options = {
     testScript = mkOption {
       type = either str (functionTo str);
-      description = ''
+      description = mdDoc ''
         A series of python declarations and statements that you write to perform
         the test.
       '';
diff --git a/nixos/maintainers/scripts/lxd/lxd-image-inner.nix b/nixos/maintainers/scripts/lxd/lxd-image-inner.nix
index c8cf2a04fb1..ee55da1e9ce 100644
--- a/nixos/maintainers/scripts/lxd/lxd-image-inner.nix
+++ b/nixos/maintainers/scripts/lxd/lxd-image-inner.nix
@@ -89,7 +89,7 @@ with lib;
 
   # 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
+  # on your system were taken. It’s perfectly fine and recommended to leave
   # this value at the release version of the first install of this system.
   # Before changing this value read the documentation for this option
   # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
diff --git a/nixos/modules/config/console.nix b/nixos/modules/config/console.nix
index f5db5dc5dfc..1e8bb78f302 100644
--- a/nixos/modules/config/console.nix
+++ b/nixos/modules/config/console.nix
@@ -21,7 +21,7 @@ let
   # Sadly, systemd-vconsole-setup doesn't support binary keymaps.
   vconsoleConf = pkgs.writeText "vconsole.conf" ''
     KEYMAP=${cfg.keyMap}
-    FONT=${cfg.font}
+    ${optionalString (cfg.font != null) "FONT=${cfg.font}"}
   '';
 
   consoleEnv = kbd: pkgs.buildEnv {
@@ -45,14 +45,19 @@ in
     };
 
     font = mkOption {
-      type = with types; either str path;
-      default = "Lat2-Terminus16";
+      type = with types; nullOr (either str path);
+      default = null;
       example = "LatArCyrHeb-16";
       description = mdDoc ''
-        The font used for the virtual consoles.  Leave empty to use
-        whatever the {command}`setfont` program considers the
-        default font.
-        Can be either a font name or a path to a PSF font file.
+        The font used for the virtual consoles.
+        Can be `null`, a font name, or a path to a PSF font file.
+
+        Use `null` to let the kernel choose a built-in font.
+        The default is 8x16, and, as of Linux 5.3, Terminus 32 bold for display
+        resolutions of 2560x1080 and higher.
+        These fonts cover the [IBM437][] character set.
+
+        [IBM437]: https://en.wikipedia.org/wiki/Code_page_437
       '';
     };
 
@@ -151,7 +156,7 @@ in
           printf "\033%%${if isUnicode then "G" else "@"}" >> /dev/console
           loadkmap < ${optimizedKeymap}
 
-          ${optionalString cfg.earlySetup ''
+          ${optionalString (cfg.earlySetup && cfg.font != null) ''
             setfont -C /dev/console $extraUtils/share/consolefonts/font.psf
           ''}
         '');
@@ -168,7 +173,7 @@ in
           "${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" # Fonts and keyboard layouts are compressed
-        ] ++ optionals (hasPrefix builtins.storeDir cfg.font) [
+        ] ++ optionals (cfg.font != null && hasPrefix builtins.storeDir cfg.font) [
           "${cfg.font}"
         ] ++ optionals (hasPrefix builtins.storeDir cfg.keyMap) [
           "${cfg.keyMap}"
@@ -195,7 +200,7 @@ in
         ];
       })
 
-      (mkIf (cfg.earlySetup && !config.boot.initrd.systemd.enable) {
+      (mkIf (cfg.earlySetup && cfg.font != null && !config.boot.initrd.systemd.enable) {
         boot.initrd.extraUtilsCommands = ''
           mkdir -p $out/share/consolefonts
           ${if substring 0 1 cfg.font == "/" then ''
diff --git a/nixos/modules/config/fonts/fontconfig.nix b/nixos/modules/config/fonts/fontconfig.nix
index f9c6e5be226..5781679241e 100644
--- a/nixos/modules/config/fonts/fontconfig.nix
+++ b/nixos/modules/config/fonts/fontconfig.nix
@@ -7,6 +7,19 @@ This module generates a package containing configuration files and link it in /e
 Fontconfig reads files in folder name / file name order, so the number prepended to the configuration file name decide the order of parsing.
 Low number means high priority.
 
+NOTE: Please take extreme care when adjusting the default settings of this module.
+People care a lot, and I mean A LOT, about their font rendering, and you will be
+The Person That Broke It if it changes in a way people don't like.
+
+See prior art:
+- https://github.com/NixOS/nixpkgs/pull/194594
+- https://github.com/NixOS/nixpkgs/pull/222236
+- https://github.com/NixOS/nixpkgs/pull/222689
+
+And do not repeat our mistakes.
+
+- @K900, March 2023
+
 */
 
 { config, pkgs, lib, ... }:
@@ -218,6 +231,8 @@ let
     paths = cfg.confPackages;
     ignoreCollisions = true;
   };
+
+  fontconfigNote = "Consider manually configuring fonts.fontconfig according to personal preference.";
 in
 {
   imports = [
@@ -229,6 +244,8 @@ in
     (mkRemovedOptionModule [ "fonts" "fontconfig" "forceAutohint" ] "")
     (mkRemovedOptionModule [ "fonts" "fontconfig" "renderMonoTTFAsBitmap" ] "")
     (mkRemovedOptionModule [ "fonts" "fontconfig" "dpi" ] "Use display server-specific options")
+    (mkRemovedOptionModule [ "hardware" "video" "hidpi" "enable" ] fontconfigNote)
+    (mkRemovedOptionModule [ "fonts" "optimizeForVeryHighDPI" ] fontconfigNote)
   ] ++ lib.forEach [ "enable" "substitutions" "preset" ]
      (opt: lib.mkRemovedOptionModule [ "fonts" "fontconfig" "ultimate" "${opt}" ] ''
        The fonts.fontconfig.ultimate module and configuration is obsolete.
diff --git a/nixos/modules/config/fonts/fonts.nix b/nixos/modules/config/fonts/fonts.nix
index c0619fa31a3..87cf837e7c8 100644
--- a/nixos/modules/config/fonts/fonts.nix
+++ b/nixos/modules/config/fonts/fonts.nix
@@ -3,29 +3,7 @@
 with lib;
 
 let
-  # A scalable variant of the X11 "core" cursor
-  #
-  # If not running a fancy desktop environment, the cursor is likely set to
-  # the default `cursor.pcf` bitmap font. This is 17px wide, so it's very
-  # small and almost invisible on 4K displays.
-  fontcursormisc_hidpi = pkgs.xorg.fontxfree86type1.overrideAttrs (old:
-    let
-      # The scaling constant is 230/96: the scalable `left_ptr` glyph at
-      # about 23 points is rendered as 17px, on a 96dpi display.
-      # Note: the XLFD font size is in decipoints.
-      size = 2.39583 * config.services.xserver.dpi;
-      sizeString = builtins.head (builtins.split "\\." (toString size));
-    in
-    {
-      postInstall = ''
-        alias='cursor -xfree86-cursor-medium-r-normal--0-${sizeString}-0-0-p-0-adobe-fontspecific'
-        echo "$alias" > $out/lib/X11/fonts/Type1/fonts.alias
-      '';
-    });
-
-  hasHidpi =
-    config.hardware.video.hidpi.enable &&
-    config.services.xserver.dpi != null;
+  cfg = config.fonts;
 
   defaultFonts =
     [ pkgs.dejavu_fonts
@@ -35,14 +13,7 @@ let
       pkgs.unifont
       pkgs.noto-fonts-emoji
     ];
-
-  defaultXFonts =
-    [ (if hasHidpi then fontcursormisc_hidpi else pkgs.xorg.fontcursormisc)
-      pkgs.xorg.fontmiscmisc
-    ];
-
 in
-
 {
   imports = [
     (mkRemovedOptionModule [ "fonts" "enableCoreFonts" ] "Use fonts.fonts = [ pkgs.corefonts ]; instead.")
@@ -68,14 +39,9 @@ in
           and families and reasonable coverage of Unicode.
         '';
       };
-
     };
 
   };
 
-  config = mkMerge [
-    { fonts.fonts = mkIf config.fonts.enableDefaultFonts defaultFonts; }
-    { fonts.fonts = mkIf config.services.xserver.enable defaultXFonts; }
-  ];
-
+  config = { fonts.fonts = mkIf cfg.enableDefaultFonts defaultFonts; };
 }
diff --git a/nixos/modules/config/malloc.nix b/nixos/modules/config/malloc.nix
index 4db0480b155..ae0661f472f 100644
--- a/nixos/modules/config/malloc.nix
+++ b/nixos/modules/config/malloc.nix
@@ -30,7 +30,7 @@ let
 
       systemPlatform = platformMap.${pkgs.stdenv.hostPlatform.system} or (throw "scudo not supported on ${pkgs.stdenv.hostPlatform.system}");
     in {
-      libPath = "${pkgs.llvmPackages_latest.compiler-rt}/lib/linux/libclang_rt.scudo-${systemPlatform}.so";
+      libPath = "${pkgs.llvmPackages_14.compiler-rt}/lib/linux/libclang_rt.scudo-${systemPlatform}.so";
       description = ''
         A user-mode allocator based on LLVM Sanitizer’s CombinedAllocator,
         which aims at providing additional mitigations against heap based
@@ -97,6 +97,7 @@ in
   };
 
   config = mkIf (cfg.provider != "libc") {
+    boot.kernel.sysctl."vm.max_map_count" = mkIf (cfg.provider == "graphene-hardened") (mkDefault 1048576);
     environment.etc."ld-nix.so.preload".text = ''
       ${providerLibPath}
     '';
diff --git a/nixos/modules/config/mysql.nix b/nixos/modules/config/mysql.nix
index af20a5e9535..2f13c56f2ae 100644
--- a/nixos/modules/config/mysql.nix
+++ b/nixos/modules/config/mysql.nix
@@ -181,7 +181,7 @@ in
                 example = "pid";
                 description = lib.mdDoc ''
                   The name of the column in the log table to which the pid of the
-                  process utilising the `pam_mysql's` authentication
+                  process utilising the `pam_mysql` authentication
                   service is stored.
                 '';
               };
diff --git a/nixos/modules/config/no-x-libs.nix b/nixos/modules/config/no-x-libs.nix
index 5a2a1a0e8ac..3ebe2fa9f16 100644
--- a/nixos/modules/config/no-x-libs.nix
+++ b/nixos/modules/config/no-x-libs.nix
@@ -30,14 +30,26 @@ with lib;
       beam = super.beam_nox;
       cairo = super.cairo.override { x11Support = false; };
       dbus = super.dbus.override { x11Support = false; };
-      ffmpeg_4 = super.ffmpeg_4-headless;
-      ffmpeg_5 = super.ffmpeg_5-headless;
+      ffmpeg_4 = super.ffmpeg_4.override { ffmpegVariant = "headless"; };
+      ffmpeg_5 = super.ffmpeg_5.override { ffmpegVariant = "headless"; };
+      # dep of graphviz, libXpm is optional for Xpm support
+      gd = super.gd.override { withXorg = false; };
       gobject-introspection = super.gobject-introspection.override { x11Support = false; };
+      gpsd = super.gpsd.override { guiSupport = false; };
+      graphviz = super.graphviz-nox;
+      gst_all_1 = super.gst_all_1 // {
+        gst-plugins-base = super.gst_all_1.gst-plugins-base.override { enableX11 = false; };
+      };
       imagemagick = super.imagemagick.override { libX11Support = false; libXtSupport = false; };
       imagemagickBig = super.imagemagickBig.override { libX11Support = false; libXtSupport = false; };
-      libextractor = super.libextractor.override { gstreamerSupport = false; gtkSupport = false; };
+      libdevil = super.libdevil-nox;
+      libextractor = super.libextractor.override { gtkSupport = false; };
       libva = super.libva-minimal;
+      limesuite = super.limesuite.override { withGui = false; };
+      mc = super.mc.override { x11Support = false; };
+      mpv-unwrapped = super.mpv-unwrapped.override { sdl2Support = false; x11Support = false; };
       msmtp = super.msmtp.override { withKeyring = false; };
+      neofetch = super.neofetch.override { x11Support = false; };
       networkmanager-fortisslvpn = super.networkmanager-fortisslvpn.override { withGnome = false; };
       networkmanager-iodine = super.networkmanager-iodine.override { withGnome = false; };
       networkmanager-l2tp = super.networkmanager-l2tp.override { withGnome = false; };
@@ -45,9 +57,17 @@ with lib;
       networkmanager-openvpn = super.networkmanager-openvpn.override { withGnome = false; };
       networkmanager-sstp = super.networkmanager-vpnc.override { withGnome = false; };
       networkmanager-vpnc = super.networkmanager-vpnc.override { withGnome = false; };
+      pango = super.pango.override { x11Support = false; };
       pinentry = super.pinentry.override { enabledFlavors = [ "curses" "tty" "emacs" ]; withLibsecret = false; };
       qemu = super.qemu.override { gtkSupport = false; spiceSupport = false; sdlSupport = false; };
       qrencode = super.qrencode.overrideAttrs (_: { doCheck = false; });
+      qt5 = super.qt5.overrideScope (const (super': {
+        qtbase = super'.qtbase.override { withGtk3 = false; };
+      }));
+      stoken = super.stoken.override { withGTK3 = false; };
+      # translateManpages -> perlPackages.po4a -> texlive-combined-basic -> texlive-core-big -> libX11
+      util-linux = super.util-linux.override { translateManpages = false; };
+      vim-full = super.vim-full.override { guiSupport = false; };
       zbar = super.zbar.override { enableVideo = false; withXorg = false; };
     }));
   };
diff --git a/nixos/modules/config/qt5.nix b/nixos/modules/config/qt.nix
index cb3180d7b96..6405166920e 100644
--- a/nixos/modules/config/qt5.nix
+++ b/nixos/modules/config/qt.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
 
-  cfg = config.qt5;
+  cfg = config.qt;
 
   isQGnome = cfg.platformTheme == "gnome" && builtins.elem cfg.style ["adwaita" "adwaita-dark"];
   isQtStyle = cfg.platformTheme == "gtk2" && !(builtins.elem cfg.style ["adwaita" "adwaita-dark"]);
@@ -12,22 +12,34 @@ let
   isLxqt = cfg.platformTheme == "lxqt";
   isKde = cfg.platformTheme == "kde";
 
-  packages = if isQGnome then [ pkgs.qgnomeplatform pkgs.adwaita-qt ]
+  packages =
+    if isQGnome then [
+      pkgs.qgnomeplatform
+      pkgs.adwaita-qt
+      pkgs.qgnomeplatform-qt6
+      pkgs.adwaita-qt6
+    ]
     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.";
+    else throw "`qt.platformTheme` ${cfg.platformTheme} and `qt.style` ${cfg.style} are not compatible.";
 
 in
 
 {
   meta.maintainers = [ maintainers.romildo ];
 
+  imports = [
+    (mkRenamedOptionModule ["qt5" "enable" ] ["qt" "enable" ])
+    (mkRenamedOptionModule ["qt5" "platformTheme" ] ["qt" "platformTheme" ])
+    (mkRenamedOptionModule ["qt5" "style" ] ["qt" "style" ])
+  ];
+
   options = {
-    qt5 = {
+    qt = {
 
-      enable = mkEnableOption (lib.mdDoc "Qt5 theming configuration");
+      enable = mkEnableOption (lib.mdDoc "Qt theming configuration");
 
       platformTheme = mkOption {
         type = types.enum [
@@ -40,13 +52,14 @@ in
         example = "gnome";
         relatedPackages = [
           "qgnomeplatform"
+          "qgnomeplatform-qt6"
           ["libsForQt5" "qtstyleplugins"]
           ["libsForQt5" "qt5ct"]
           ["lxqt" "lxqt-qtplugin"]
           ["libsForQt5" "plasma-integration"]
         ];
         description = lib.mdDoc ''
-          Selects the platform theme to use for Qt5 applications.
+          Selects the platform theme to use for Qt applications.
 
           The options are
           - `gtk`: Use GTK theme with [qtstyleplugins](https://github.com/qt/qtstyleplugins)
@@ -71,10 +84,11 @@ in
         example = "adwaita";
         relatedPackages = [
           "adwaita-qt"
+          "adwaita-qt6"
           ["libsForQt5" "qtstyleplugins"]
         ];
         description = lib.mdDoc ''
-          Selects the style to use for Qt5 applications.
+          Selects the style to use for Qt applications.
 
           The options are
           - `adwaita`, `adwaita-dark`: Use Adwaita Qt style with
@@ -88,9 +102,17 @@ in
 
   config = mkIf cfg.enable {
 
-    environment.variables.QT_QPA_PLATFORMTHEME = cfg.platformTheme;
+    environment.variables = {
+      QT_QPA_PLATFORMTHEME = cfg.platformTheme;
+      QT_STYLE_OVERRIDE = mkIf (! (isQt5ct || isLxqt || isKde)) cfg.style;
+    };
 
-    environment.variables.QT_STYLE_OVERRIDE = mkIf (! (isQt5ct || isLxqt || isKde)) cfg.style;
+    environment.profileRelativeSessionVariables = let
+      qtVersions = with pkgs; [ qt5 qt6 ];
+    in {
+      QT_PLUGIN_PATH = map (qt: "/${qt.qtbase.qtPluginPrefix}") qtVersions;
+      QML2_IMPORT_PATH = map (qt: "/${qt.qtbase.qtQmlPrefix}") qtVersions;
+    };
 
     environment.systemPackages = packages;
 
diff --git a/nixos/modules/config/resolvconf.nix b/nixos/modules/config/resolvconf.nix
index 76605a063a4..e9ae4d651d2 100644
--- a/nixos/modules/config/resolvconf.nix
+++ b/nixos/modules/config/resolvconf.nix
@@ -132,13 +132,13 @@ in
             exit 1
           ''
         else configText;
-
-      environment.systemPackages = [ cfg.package ];
     }
 
     (mkIf cfg.enable {
       networking.resolvconf.package = pkgs.openresolv;
 
+      environment.systemPackages = [ cfg.package ];
+
       systemd.services.resolvconf = {
         description = "resolvconf update";
 
diff --git a/nixos/modules/config/shells-environment.nix b/nixos/modules/config/shells-environment.nix
index 50bb9b17783..bc6583442ed 100644
--- a/nixos/modules/config/shells-environment.nix
+++ b/nixos/modules/config/shells-environment.nix
@@ -42,8 +42,8 @@ in
         strings.  The latter is concatenated, interspersed with colon
         characters.
       '';
-      type = with types; attrsOf (either str (listOf str));
-      apply = mapAttrs (n: v: if isList v then concatStringsSep ":" v else v);
+      type = with types; attrsOf (oneOf [ (listOf str) str path ]);
+      apply = mapAttrs (n: v: if isList v then concatStringsSep ":" v else "${v}");
     };
 
     environment.profiles = mkOption {
diff --git a/nixos/modules/config/stevenblack.nix b/nixos/modules/config/stevenblack.nix
new file mode 100644
index 00000000000..07a0aa339a5
--- /dev/null
+++ b/nixos/modules/config/stevenblack.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) optionals mkOption mkEnableOption types mkIf elem concatStringsSep maintainers mdDoc;
+  cfg = config.networking.stevenblack;
+
+  # needs to be in a specific order
+  activatedHosts = with cfg; [ ]
+    ++ optionals (elem "fakenews" block) [ "fakenews" ]
+    ++ optionals (elem "gambling" block) [ "gambling" ]
+    ++ optionals (elem "porn" block) [ "porn" ]
+    ++ optionals (elem "social" block) [ "social" ];
+
+  hostsPath = "${pkgs.stevenblack-blocklist}/alternates/" + concatStringsSep "-" activatedHosts + "/hosts";
+in
+{
+  options.networking.stevenblack = {
+    enable = mkEnableOption (mdDoc "Enable the stevenblack hosts file blocklist");
+
+    block = mkOption {
+      type = types.listOf (types.enum [ "fakenews" "gambling" "porn" "social" ]);
+      default = [ ];
+      description = mdDoc "Additional blocklist extensions.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.hostFiles = [ ]
+      ++ optionals (activatedHosts != [ ]) [ hostsPath ]
+      ++ optionals (activatedHosts == [ ]) [ "${pkgs.stevenblack-blocklist}/hosts" ];
+  };
+
+  meta.maintainers = [ maintainers.fortuneteller2k maintainers.artturin ];
+}
diff --git a/nixos/modules/config/swap.nix b/nixos/modules/config/swap.nix
index 76a054b100e..2c9c4c9c1a2 100644
--- a/nixos/modules/config/swap.nix
+++ b/nixos/modules/config/swap.nix
@@ -66,7 +66,7 @@ let
 
       device = mkOption {
         example = "/dev/sda3";
-        type = types.str;
+        type = types.nonEmptyStr;
         description = lib.mdDoc "Path of the device or swap file.";
       };
 
@@ -197,6 +197,21 @@ in
   };
 
   config = mkIf ((length config.swapDevices) != 0) {
+    assertions = map (sw: {
+      assertion = sw.randomEncryption.enable -> builtins.match "/dev/disk/by-(uuid|label)/.*" sw.device == null;
+      message = ''
+        You cannot use swap device "${sw.device}" with randomEncryption enabled.
+        The UUIDs and labels will get erased on every boot when the partition is encrypted.
+        Use /dev/disk/by-partuuid/… instead.
+      '';
+    }) config.swapDevices;
+
+    warnings =
+      concatMap (sw:
+        if sw.size != null && hasPrefix "/dev/" sw.device
+        then [ "Setting the swap size of block device ${sw.device} has no effect" ]
+        else [ ])
+      config.swapDevices;
 
     system.requiredKernelConfig = with config.lib.kernelConfig; [
       (isYes "SWAP")
@@ -205,24 +220,27 @@ in
     # Create missing swapfiles.
     systemd.services =
       let
-
         createSwapDevice = sw:
-          assert sw.device != "";
-          assert !(sw.randomEncryption.enable && lib.hasPrefix "/dev/disk/by-uuid"  sw.device);
-          assert !(sw.randomEncryption.enable && lib.hasPrefix "/dev/disk/by-label" sw.device);
           let realDevice' = escapeSystemdPath sw.realDevice;
           in nameValuePair "mkswap-${sw.deviceName}"
           { description = "Initialisation of swap device ${sw.device}";
             wantedBy = [ "${realDevice'}.swap" ];
             before = [ "${realDevice'}.swap" ];
-            path = [ pkgs.util-linux ] ++ optional sw.randomEncryption.enable pkgs.cryptsetup;
+            path = [ pkgs.util-linux pkgs.e2fsprogs ]
+              ++ optional sw.randomEncryption.enable pkgs.cryptsetup;
+
+            environment.DEVICE = sw.device;
 
             script =
               ''
                 ${optionalString (sw.size != null) ''
-                  currentSize=$(( $(stat -c "%s" "${sw.device}" 2>/dev/null || echo 0) / 1024 / 1024 ))
-                  if [ "${toString sw.size}" != "$currentSize" ]; then
-                    dd if=/dev/zero of="${sw.device}" bs=1M count=${toString sw.size}
+                  currentSize=$(( $(stat -c "%s" "$DEVICE" 2>/dev/null || echo 0) / 1024 / 1024 ))
+                  if [[ ! -b "$DEVICE" && "${toString sw.size}" != "$currentSize" ]]; then
+                    # Disable CoW for CoW based filesystems like BTRFS.
+                    truncate --size 0 "$DEVICE"
+                    chattr +C "$DEVICE" 2>/dev/null || true
+
+                    dd if=/dev/zero of="$DEVICE" bs=1M count=${toString sw.size}
                     chmod 0600 ${sw.device}
                     ${optionalString (!sw.randomEncryption.enable) "mkswap ${sw.realDevice}"}
                   fi
diff --git a/nixos/modules/config/system-environment.nix b/nixos/modules/config/system-environment.nix
index 5b226d5079b..39930418522 100644
--- a/nixos/modules/config/system-environment.nix
+++ b/nixos/modules/config/system-environment.nix
@@ -1,6 +1,6 @@
 # This module defines a system-wide environment that will be
 # initialised by pam_env (that is, not only in shells).
-{ config, lib, pkgs, ... }:
+{ config, lib, options, pkgs, ... }:
 
 with lib;
 
@@ -32,8 +32,7 @@ in
         therefore not possible to use PAM style variables such as
         `@{HOME}`.
       '';
-      type = with types; attrsOf (either str (listOf str));
-      apply = mapAttrs (n: v: if isList v then concatStringsSep ":" v else v);
+      inherit (options.environment.variables) type apply;
     };
 
     environment.profileRelativeSessionVariables = mkOption {
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl
index 4368ec24ea9..54352a517a2 100644
--- a/nixos/modules/config/update-users-groups.pl
+++ b/nixos/modules/config/update-users-groups.pl
@@ -215,10 +215,12 @@ foreach my $u (@{$spec->{users}}) {
     } else {
         $u->{uid} = allocUid($name, $u->{isSystemUser}) if !defined $u->{uid};
 
-        if (defined $u->{initialPassword}) {
-            $u->{hashedPassword} = hashPassword($u->{initialPassword});
-        } elsif (defined $u->{initialHashedPassword}) {
-            $u->{hashedPassword} = $u->{initialHashedPassword};
+        if (!defined $u->{hashedPassword}) {
+            if (defined $u->{initialPassword}) {
+                $u->{hashedPassword} = hashPassword($u->{initialPassword});
+            } elsif (defined $u->{initialHashedPassword}) {
+                $u->{hashedPassword} = $u->{initialHashedPassword};
+            }
         }
     }
 
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix
index 61d70ccc19b..d1e9c8072ea 100644
--- a/nixos/modules/config/users-groups.nix
+++ b/nixos/modules/config/users-groups.nix
@@ -90,7 +90,7 @@ let
           only has an effect if {option}`uid` is
           {option}`null`, in which case it determines whether
           the user's UID is allocated in the range for system users
-          (below 500) or in the range for normal users (starting at
+          (below 1000) or in the range for normal users (starting at
           1000).
           Exactly one of `isNormalUser` and
           `isSystemUser` must be true.
@@ -101,16 +101,13 @@ let
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Indicates whether this is an account for a “real” user. This
-          automatically sets {option}`group` to
-          `users`, {option}`createHome` to
-          `true`, {option}`home` to
-          {file}`/home/«username»`,
+          Indicates whether this is an account for a “real” user.
+          This automatically sets {option}`group` to `users`,
+          {option}`createHome` to `true`,
+          {option}`home` to {file}`/home/«username»`,
           {option}`useDefaultShell` to `true`,
-          and {option}`isSystemUser` to
-          `false`.
-          Exactly one of `isNormalUser` and
-          `isSystemUser` must be true.
+          and {option}`isSystemUser` to `false`.
+          Exactly one of `isNormalUser` and `isSystemUser` must be true.
         '';
       };
 
@@ -276,6 +273,9 @@ let
           {command}`passwd` command. Otherwise, it's
           equivalent to setting the {option}`hashedPassword` option.
 
+          Note that the {option}`hashedPassword` option will override
+          this option if both are set.
+
           ${hashedPasswordDescription}
         '';
       };
@@ -294,6 +294,9 @@ let
           is world-readable in the Nix store, so it should only be
           used for guest accounts or passwords that will be changed
           promptly.
+
+          Note that the {option}`password` option will override this
+          option if both are set.
         '';
       };
 
@@ -425,6 +428,8 @@ let
 
   uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid";
   gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid";
+  sdInitrdUidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) config.boot.initrd.systemd.users) "uid";
+  sdInitrdGidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) config.boot.initrd.systemd.groups) "gid";
 
   spec = pkgs.writeText "users-groups.json" (builtins.toJSON {
     inherit (cfg) mutableUsers;
@@ -447,8 +452,8 @@ let
 
 in {
   imports = [
-    (mkAliasOptionModule [ "users" "extraUsers" ] [ "users" "users" ])
-    (mkAliasOptionModule [ "users" "extraGroups" ] [ "users" "groups" ])
+    (mkAliasOptionModuleMD [ "users" "extraUsers" ] [ "users" "users" ])
+    (mkAliasOptionModuleMD [ "users" "extraGroups" ] [ "users" "groups" ])
     (mkRenamedOptionModule ["security" "initialRootPassword"] ["users" "users" "root" "initialHashedPassword"])
   ];
 
@@ -531,12 +536,62 @@ in {
         WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing.
       '';
     };
+
+    # systemd initrd
+    boot.initrd.systemd.users = mkOption {
+      visible = false;
+      description = ''
+        Users to include in initrd.
+      '';
+      default = {};
+      type = types.attrsOf (types.submodule ({ name, ... }: {
+        options.uid = mkOption {
+          visible = false;
+          type = types.int;
+          description = ''
+            ID of the user in initrd.
+          '';
+          defaultText = literalExpression "config.users.users.\${name}.uid";
+          default = cfg.users.${name}.uid;
+        };
+        options.group = mkOption {
+          visible = false;
+          type = types.singleLineStr;
+          description = ''
+            Group the user belongs to in initrd.
+          '';
+          defaultText = literalExpression "config.users.users.\${name}.group";
+          default = cfg.users.${name}.group;
+        };
+      }));
+    };
+
+    boot.initrd.systemd.groups = mkOption {
+      visible = false;
+      description = ''
+        Groups to include in initrd.
+      '';
+      default = {};
+      type = types.attrsOf (types.submodule ({ name, ... }: {
+        options.gid = mkOption {
+          visible = false;
+          type = types.int;
+          description = ''
+            ID of the group in initrd.
+          '';
+          defaultText = literalExpression "config.users.groups.\${name}.gid";
+          default = cfg.groups.${name}.gid;
+        };
+      }));
+    };
   };
 
 
   ###### implementation
 
-  config = {
+  config = let
+    cryptSchemeIdPatternGroup = "(${lib.concatStringsSep "|" pkgs.libxcrypt.enabledCryptSchemeIds})";
+  in {
 
     users.users = {
       root = {
@@ -598,15 +653,16 @@ in {
       text = ''
         users=()
         while IFS=: read -r user hash tail; do
-          if [[ "$hash" = "$"* && ! "$hash" =~ ^\$(y|gy|7|2b|2y|2a|6)\$ ]]; then
+          if [[ "$hash" = "$"* && ! "$hash" =~ ^\''$${cryptSchemeIdPatternGroup}\$ ]]; then
             users+=("$user")
           fi
         done </etc/shadow
 
         if (( "''${#users[@]}" )); then
           echo "
-        WARNING: The following user accounts rely on password hashes that will
-        be removed in NixOS 23.05. They should be renewed as soon as possible."
+        WARNING: The following user accounts rely on password hashing algorithms
+        that have been removed. They need to be renewed as soon as possible, as
+        they do prevent their users from logging in."
           printf ' - %s\n' "''${users[@]}"
         fi
       '';
@@ -633,10 +689,52 @@ in {
       "/etc/profiles/per-user/$USER"
     ];
 
+    # systemd initrd
+    boot.initrd.systemd = lib.mkIf config.boot.initrd.systemd.enable {
+      contents = {
+        "/etc/passwd".text = ''
+          ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { uid, group }: let
+            g = config.boot.initrd.systemd.groups.${group};
+          in "${n}:x:${toString uid}:${toString g.gid}::/var/empty:") config.boot.initrd.systemd.users)}
+        '';
+        "/etc/group".text = ''
+          ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { gid }: "${n}:x:${toString gid}:") config.boot.initrd.systemd.groups)}
+        '';
+      };
+
+      users = {
+        root = {};
+        nobody = {};
+      };
+
+      groups = {
+        root = {};
+        nogroup = {};
+        systemd-journal = {};
+        tty = {};
+        dialout = {};
+        kmem = {};
+        input = {};
+        video = {};
+        render = {};
+        sgx = {};
+        audio = {};
+        video = {};
+        lp = {};
+        disk = {};
+        cdrom = {};
+        tape = {};
+        kvm = {};
+      };
+    };
+
     assertions = [
       { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique);
         message = "UIDs and GIDs must be unique!";
       }
+      { assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique);
+        message = "systemd initrd UIDs and GIDs must be unique!";
+      }
       { # If mutableUsers is false, to prevent users creating a
         # configuration that locks them out of the system, ensure that
         # there is at least one "privileged" account that has a
@@ -680,7 +778,7 @@ in {
           {
             assertion = let
               xor = a: b: a && !b || b && !a;
-              isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 500);
+              isEffectivelySystemUser = user.isSystemUser || (user.uid != null && user.uid < 1000);
             in xor isEffectivelySystemUser user.isNormalUser;
             message = ''
               Exactly one of users.users.${user.name}.isSystemUser and users.users.${user.name}.isNormalUser must be set.
@@ -696,7 +794,20 @@ in {
               users.groups.${user.name} = {};
             '';
           }
-        ]
+        ] ++ (map (shell: {
+            assertion = (user.shell == pkgs.${shell}) -> (config.programs.${shell}.enable == true);
+            message = ''
+              users.users.${user.name}.shell is set to ${shell}, but
+              programs.${shell}.enable is not true. This will cause the ${shell}
+              shell to lack the basic nix directories in its PATH and might make
+              logging in as that user impossible. You can fix it with:
+              programs.${shell}.enable = true;
+            '';
+          }) [
+          "fish"
+          "xonsh"
+          "zsh"
+        ])
     ));
 
     warnings =
@@ -713,9 +824,10 @@ in {
         let
           sep = "\\$";
           base64 = "[a-zA-Z0-9./]+";
-          id = "[a-z0-9-]+";
+          id = cryptSchemeIdPatternGroup;
+          name = "[a-z0-9-]+";
           value = "[a-zA-Z0-9/+.-]+";
-          options = "${id}(=${value})?(,${id}=${value})*";
+          options = "${name}(=${value})?(,${name}=${value})*";
           scheme  = "${id}(${sep}${options})?";
           content = "${base64}${sep}${base64}(${sep}${base64})?";
           mcf = "^${sep}${scheme}${sep}${content}$";
diff --git a/nixos/modules/config/zram.nix b/nixos/modules/config/zram.nix
index 87ac53a60b7..991387ea9b2 100644
--- a/nixos/modules/config/zram.nix
+++ b/nixos/modules/config/zram.nix
@@ -1,45 +1,27 @@
 { config, lib, pkgs, ... }:
 
-with lib;
-
 let
 
   cfg = config.zramSwap;
-
-  # don't set swapDevices as mkDefault, so we can detect user had read our warning
-  # (see below) and made an action (or not)
-  devicesCount = if cfg.swapDevices != null then cfg.swapDevices else cfg.numDevices;
-
-  devices = map (nr: "zram${toString nr}") (range 0 (devicesCount - 1));
-
-  modprobe = "${pkgs.kmod}/bin/modprobe";
-
-  warnings =
-  assert cfg.swapDevices != null -> cfg.numDevices >= cfg.swapDevices;
-  flatten [
-    (optional (cfg.numDevices > 1 && cfg.swapDevices == null) ''
-      Using several small zram devices as swap is no better than using one large.
-      Set either zramSwap.numDevices = 1 or explicitly set zramSwap.swapDevices.
-
-      Previously multiple zram devices were used to enable multithreaded
-      compression. Linux supports multithreaded compression for 1 device
-      since 3.15. See https://lkml.org/lkml/2014/2/28/404 for details.
-    '')
-  ];
+  devices = map (nr: "zram${toString nr}") (lib.range 0 (cfg.swapDevices - 1));
 
 in
 
 {
 
+  imports = [
+    (lib.mkRemovedOptionModule [ "zramSwap" "numDevices" ] "Using ZRAM devices as general purpose ephemeral block devices is no longer supported")
+  ];
+
   ###### interface
 
   options = {
 
     zramSwap = {
 
-      enable = mkOption {
+      enable = lib.mkOption {
         default = false;
-        type = types.bool;
+        type = lib.types.bool;
         description = lib.mdDoc ''
           Enable in-memory compressed devices and swap space provided by the zram
           kernel module.
@@ -49,29 +31,17 @@ in
         '';
       };
 
-      numDevices = mkOption {
+      swapDevices = lib.mkOption {
         default = 1;
-        type = types.int;
+        type = lib.types.int;
         description = lib.mdDoc ''
-          Number of zram devices to create. See also
-          `zramSwap.swapDevices`
+          Number of zram devices to be used as swap, recommended is 1.
         '';
       };
 
-      swapDevices = mkOption {
-        default = null;
-        example = 1;
-        type = with types; nullOr int;
-        description = lib.mdDoc ''
-          Number of zram devices to be used as swap. Must be
-          `<= zramSwap.numDevices`.
-          Default is same as `zramSwap.numDevices`, recommended is 1.
-        '';
-      };
-
-      memoryPercent = mkOption {
+      memoryPercent = lib.mkOption {
         default = 50;
-        type = types.int;
+        type = lib.types.int;
         description = lib.mdDoc ''
           Maximum total amount of memory that can be stored in the zram swap devices
           (as a percentage of your total memory). Defaults to 1/2 of your total
@@ -80,9 +50,9 @@ in
         '';
       };
 
-      memoryMax = mkOption {
+      memoryMax = lib.mkOption {
         default = null;
-        type = with types; nullOr int;
+        type = with lib.types; nullOr int;
         description = lib.mdDoc ''
           Maximum total amount of memory (in bytes) that can be stored in the zram
           swap devices.
@@ -90,9 +60,9 @@ in
         '';
       };
 
-      priority = mkOption {
+      priority = lib.mkOption {
         default = 5;
-        type = types.int;
+        type = lib.types.int;
         description = lib.mdDoc ''
           Priority of the zram swap devices. It should be a number higher than
           the priority of your disk-based swap devices (so that the system will
@@ -100,10 +70,10 @@ in
         '';
       };
 
-      algorithm = mkOption {
+      algorithm = lib.mkOption {
         default = "zstd";
         example = "lz4";
-        type = with types; either (enum [ "lzo" "lz4" "zstd" ]) str;
+        type = with lib.types; either (enum [ "lzo" "lz4" "zstd" ]) str;
         description = lib.mdDoc ''
           Compression algorithm. `lzo` has good compression,
           but is slow. `lz4` has bad compression, but is fast.
@@ -112,13 +82,29 @@ in
           {command}`cat /sys/class/block/zram*/comp_algorithm`
         '';
       };
+
+      writebackDevice = lib.mkOption {
+        default = null;
+        example = "/dev/zvol/tarta-zoot/swap-writeback";
+        type = lib.types.nullOr lib.types.path;
+        description = lib.mdDoc ''
+          Write incompressible pages to this device,
+          as there's no gain from keeping them in RAM.
+        '';
+      };
     };
 
   };
 
-  config = mkIf cfg.enable {
+  config = lib.mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = cfg.writebackDevice == null || cfg.swapDevices <= 1;
+        message = "A single writeback device cannot be shared among multiple zram devices";
+      }
+    ];
 
-    inherit warnings;
 
     system.requiredKernelConfig = with config.lib.kernelConfig; [
       (isModule "ZRAM")
@@ -128,78 +114,27 @@ in
     # once in stage 2 boot, and again when the zram-reloader service starts.
     # boot.kernelModules = [ "zram" ];
 
-    boot.extraModprobeConfig = ''
-      options zram num_devices=${toString cfg.numDevices}
-    '';
-
-    boot.kernelParams = ["zram.num_devices=${toString cfg.numDevices}"];
-
-    services.udev.extraRules = ''
-      KERNEL=="zram[0-9]*", ENV{SYSTEMD_WANTS}="zram-init-%k.service", TAG+="systemd"
-    '';
-
-    systemd.services =
-      let
-        createZramInitService = dev:
-          nameValuePair "zram-init-${dev}" {
-            description = "Init swap on zram-based device ${dev}";
-            after = [ "dev-${dev}.device" "zram-reloader.service" ];
-            requires = [ "dev-${dev}.device" "zram-reloader.service" ];
-            before = [ "dev-${dev}.swap" ];
-            requiredBy = [ "dev-${dev}.swap" ];
-            unitConfig.DefaultDependencies = false; # needed to prevent a cycle
-            serviceConfig = {
-              Type = "oneshot";
-              RemainAfterExit = true;
-              ExecStop = "${pkgs.runtimeShell} -c 'echo 1 > /sys/class/block/${dev}/reset'";
-            };
-            script = ''
-              set -euo pipefail
-
-              # Calculate memory to use for zram
-              mem=$(${pkgs.gawk}/bin/awk '/MemTotal: / {
-                  value=int($2*${toString cfg.memoryPercent}/100.0/${toString devicesCount}*1024);
-                    ${lib.optionalString (cfg.memoryMax != null) ''
-                      memory_max=int(${toString cfg.memoryMax}/${toString devicesCount});
-                      if (value > memory_max) { value = memory_max }
-                    ''}
-                  print value
-              }' /proc/meminfo)
-
-              ${pkgs.util-linux}/sbin/zramctl --size $mem --algorithm ${cfg.algorithm} /dev/${dev}
-              ${pkgs.util-linux}/sbin/mkswap /dev/${dev}
-            '';
-            restartIfChanged = false;
-          };
-      in listToAttrs ((map createZramInitService devices) ++ [(nameValuePair "zram-reloader"
-        {
-          description = "Reload zram kernel module when number of devices changes";
-          wants = [ "systemd-udevd.service" ];
-          after = [ "systemd-udevd.service" ];
-          unitConfig.DefaultDependencies = false; # needed to prevent a cycle
-          serviceConfig = {
-            Type = "oneshot";
-            RemainAfterExit = true;
-            ExecStartPre = "-${modprobe} -r zram";
-            ExecStart = "-${modprobe} zram";
-            ExecStop = "-${modprobe} -r zram";
-          };
-          restartTriggers = [
-            cfg.numDevices
-            cfg.algorithm
-            cfg.memoryPercent
-          ];
-          restartIfChanged = true;
-        })]);
-
-    swapDevices =
-      let
-        useZramSwap = dev:
-          {
-            device = "/dev/${dev}";
-            priority = cfg.priority;
-          };
-      in map useZramSwap devices;
+    systemd.packages = [ pkgs.zram-generator ];
+    systemd.services."systemd-zram-setup@".path = [ pkgs.util-linux ]; # for mkswap
+
+    environment.etc."systemd/zram-generator.conf".source =
+      (pkgs.formats.ini { }).generate "zram-generator.conf" (lib.listToAttrs
+        (builtins.map
+          (dev: {
+            name = dev;
+            value =
+              let
+                size = "${toString cfg.memoryPercent} / 100 * ram";
+              in
+              {
+                zram-size = if cfg.memoryMax != null then "min(${size}, ${toString cfg.memoryMax} / 1024 / 1024)" else size;
+                compression-algorithm = cfg.algorithm;
+                swap-priority = cfg.priority;
+              } // lib.optionalAttrs (cfg.writebackDevice != null) {
+                writeback-device = cfg.writebackDevice;
+              };
+          })
+          devices));
 
   };
 
diff --git a/nixos/modules/hardware/all-firmware.nix b/nixos/modules/hardware/all-firmware.nix
index 2d5a0007ff0..75247286368 100644
--- a/nixos/modules/hardware/all-firmware.nix
+++ b/nixos/modules/hardware/all-firmware.nix
@@ -65,8 +65,6 @@ in {
       ] ++ optional pkgs.stdenv.hostPlatform.isAarch raspberrypiWirelessFirmware
         ++ optionals (versionOlder config.boot.kernelPackages.kernel.version "4.13") [
         rtl8723bs-firmware
-      ] ++ optionals (versionOlder config.boot.kernelPackages.kernel.version "5.16") [
-        rtw89-firmware
       ];
       hardware.wirelessRegulatoryDatabase = true;
     })
diff --git a/nixos/modules/hardware/device-tree.nix b/nixos/modules/hardware/device-tree.nix
index 2807313a5a9..c568f52ab67 100644
--- a/nixos/modules/hardware/device-tree.nix
+++ b/nixos/modules/hardware/device-tree.nix
@@ -65,7 +65,7 @@ let
     };
   };
 
-  filterDTBs = src: if isNull cfg.filter
+  filterDTBs = src: if cfg.filter == null
     then "${src}/dtbs"
     else
       pkgs.runCommand "dtbs-filtered" {} ''
@@ -93,8 +93,8 @@ let
   # Fill in `dtboFile` for each overlay if not set already.
   # Existence of one of these is guarded by assertion below
   withDTBOs = xs: flip map xs (o: o // { dtboFile =
-    if isNull o.dtboFile then
-      if !isNull o.dtsFile then compileDTS o.name o.dtsFile
+    if o.dtboFile == null then
+      if o.dtsFile != null then compileDTS o.name o.dtsFile
       else compileDTS o.name (pkgs.writeText "dts" o.dtsText)
     else o.dtboFile; } );
 
@@ -181,7 +181,7 @@ in
   config = mkIf (cfg.enable) {
 
     assertions = let
-      invalidOverlay = o: isNull o.dtsFile && isNull o.dtsText && isNull o.dtboFile;
+      invalidOverlay = o: (o.dtsFile == null) && (o.dtsText == null) && (o.dtboFile == null);
     in lib.singleton {
       assertion = lib.all (o: !invalidOverlay o) cfg.overlays;
       message = ''
diff --git a/nixos/modules/hardware/flipperzero.nix b/nixos/modules/hardware/flipperzero.nix
new file mode 100644
index 00000000000..82f9b76fa3a
--- /dev/null
+++ b/nixos/modules/hardware/flipperzero.nix
@@ -0,0 +1,18 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.hardware.flipperzero;
+
+in
+
+{
+  options.hardware.flipperzero.enable = mkEnableOption (mdDoc "udev rules and software for Flipper Zero devices");
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.qFlipper ];
+    services.udev.packages = [ pkgs.qFlipper ];
+  };
+}
diff --git a/nixos/modules/hardware/keyboard/qmk.nix b/nixos/modules/hardware/keyboard/qmk.nix
new file mode 100644
index 00000000000..df3bcaeccd2
--- /dev/null
+++ b/nixos/modules/hardware/keyboard/qmk.nix
@@ -0,0 +1,16 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.hardware.keyboard.qmk;
+  inherit (lib) mdDoc mkEnableOption mkIf;
+
+in
+{
+  options.hardware.keyboard.qmk = {
+    enable = mkEnableOption (mdDoc "non-root access to the firmware of QMK keyboards");
+  };
+
+  config = mkIf cfg.enable {
+    services.udev.packages = [ pkgs.qmk-udev-rules ];
+  };
+}
diff --git a/nixos/modules/hardware/keyboard/teck.nix b/nixos/modules/hardware/keyboard/teck.nix
index 2705668d9a7..8376c6b9c50 100644
--- a/nixos/modules/hardware/keyboard/teck.nix
+++ b/nixos/modules/hardware/keyboard/teck.nix
@@ -1,16 +1,16 @@
 { config, lib, pkgs, ... }:
 
-with lib;
 let
   cfg = config.hardware.keyboard.teck;
+  inherit (lib) mdDoc mkEnableOption mkIf;
+
 in
 {
   options.hardware.keyboard.teck = {
-    enable = mkEnableOption (lib.mdDoc "non-root access to the firmware of TECK keyboards");
+    enable = mkEnableOption (mdDoc "non-root access to the firmware of TECK keyboards");
   };
 
   config = mkIf cfg.enable {
     services.udev.packages = [ pkgs.teck-udev-rules ];
   };
 }
-
diff --git a/nixos/modules/hardware/keyboard/uhk.nix b/nixos/modules/hardware/keyboard/uhk.nix
index c1805143993..17baff83d88 100644
--- a/nixos/modules/hardware/keyboard/uhk.nix
+++ b/nixos/modules/hardware/keyboard/uhk.nix
@@ -1,13 +1,14 @@
 { config, lib, pkgs, ... }:
 
-with lib;
 let
   cfg = config.hardware.keyboard.uhk;
+  inherit (lib) mdDoc mkEnableOption mkIf;
+
 in
 {
   options.hardware.keyboard.uhk = {
-    enable = mkEnableOption (lib.mdDoc ''
-    non-root access to the firmware of UHK keyboards.
+    enable = mkEnableOption (mdDoc ''
+      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.
diff --git a/nixos/modules/hardware/keyboard/zsa.nix b/nixos/modules/hardware/keyboard/zsa.nix
index 5bf4022cdc4..a04b67b5c8d 100644
--- a/nixos/modules/hardware/keyboard/zsa.nix
+++ b/nixos/modules/hardware/keyboard/zsa.nix
@@ -1,21 +1,18 @@
 { config, lib, pkgs, ... }:
 
 let
-  inherit (lib) mkOption mkIf types;
   cfg = config.hardware.keyboard.zsa;
+  inherit (lib) mkEnableOption mkIf mdDoc;
+
 in
 {
   options.hardware.keyboard.zsa = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Enables udev rules for keyboards from ZSA like the ErgoDox EZ, Planck EZ and Moonlander Mark I.
-        You need it when you want to flash a new configuration on the keyboard
-        or use their live training in the browser.
-        You may want to install the wally-cli package.
-      '';
-    };
+    enable = mkEnableOption (mdDoc ''
+      udev rules for keyboards from ZSA like the ErgoDox EZ, Planck EZ and Moonlander Mark I.
+      You need it when you want to flash a new configuration on the keyboard
+      or use their live training in the browser.
+      You may want to install the wally-cli package.
+    '');
   };
 
   config = mkIf cfg.enable {
diff --git a/nixos/modules/hardware/nitrokey.nix b/nixos/modules/hardware/nitrokey.nix
index fa9dd4d6d8f..e2e88a8eade 100644
--- a/nixos/modules/hardware/nitrokey.nix
+++ b/nixos/modules/hardware/nitrokey.nix
@@ -22,6 +22,6 @@ in
   };
 
   config = mkIf cfg.enable {
-    services.udev.packages = [ pkgs.nitrokey-udev-rules ];
+    services.udev.packages = [ pkgs.libnitrokey ];
   };
 }
diff --git a/nixos/modules/hardware/opengl.nix b/nixos/modules/hardware/opengl.nix
index 5a5d88d9a4e..9108bcbd165 100644
--- a/nixos/modules/hardware/opengl.nix
+++ b/nixos/modules/hardware/opengl.nix
@@ -26,9 +26,7 @@ in
 
   imports = [
     (mkRenamedOptionModule [ "services" "xserver" "vaapiDrivers" ] [ "hardware" "opengl" "extraPackages" ])
-    (mkRemovedOptionModule [ "hardware" "opengl" "s3tcSupport" ] ''
-      S3TC support is now always enabled in Mesa.
-    '')
+    (mkRemovedOptionModule [ "hardware" "opengl" "s3tcSupport" ] "S3TC support is now always enabled in Mesa.")
   ];
 
   options = {
@@ -89,21 +87,28 @@ in
       extraPackages = mkOption {
         type = types.listOf types.package;
         default = [];
-        example = literalExpression "with pkgs; [ vaapiIntel libvdpau-va-gl vaapiVdpau intel-ocl ]";
+        example = literalExpression "with pkgs; [ intel-media-driver intel-ocl vaapiIntel ]";
         description = lib.mdDoc ''
-          Additional packages to add to OpenGL drivers. This can be used
-          to add OpenCL drivers, VA-API/VDPAU drivers etc.
+          Additional packages to add to OpenGL drivers.
+          This can be used to add OpenCL drivers, VA-API/VDPAU drivers etc.
+
+          ::: {.note}
+          intel-media-driver supports hardware Broadwell (2014) or newer. Older hardware should use the mostly unmaintained vaapiIntel driver.
+          :::
         '';
       };
 
       extraPackages32 = mkOption {
         type = types.listOf types.package;
         default = [];
-        example = literalExpression "with pkgs.pkgsi686Linux; [ vaapiIntel libvdpau-va-gl vaapiVdpau ]";
+        example = literalExpression "with pkgs.pkgsi686Linux; [ intel-media-driver vaapiIntel ]";
         description = lib.mdDoc ''
-          Additional packages to add to 32-bit OpenGL drivers on
-          64-bit systems. Used when {option}`driSupport32Bit` is
-          set. This can be used to add OpenCL drivers, VA-API/VDPAU drivers etc.
+          Additional packages to add to 32-bit OpenGL drivers on 64-bit systems.
+          Used when {option}`driSupport32Bit` is set. This can be used to add OpenCL drivers, VA-API/VDPAU drivers etc.
+
+          ::: {.note}
+          intel-media-driver supports hardware Broadwell (2014) or newer. Older hardware should use the mostly unmaintained vaapiIntel driver.
+          :::
         '';
       };
 
@@ -124,7 +129,6 @@ in
   };
 
   config = mkIf cfg.enable {
-
     assertions = [
       { assertion = cfg.driSupport32Bit -> pkgs.stdenv.isx86_64;
         message = "Option driSupport32Bit only makes sense on a 64-bit system.";
diff --git a/nixos/modules/hardware/opentabletdriver.nix b/nixos/modules/hardware/opentabletdriver.nix
index 6c5ca3d949e..e3f418abce4 100644
--- a/nixos/modules/hardware/opentabletdriver.nix
+++ b/nixos/modules/hardware/opentabletdriver.nix
@@ -61,7 +61,7 @@ in
 
       serviceConfig = {
         Type = "simple";
-        ExecStart = "${cfg.package}/bin/otd-daemon -c ${cfg.package}/lib/OpenTabletDriver/Configurations";
+        ExecStart = "${cfg.package}/bin/otd-daemon";
         Restart = "on-failure";
       };
     };
diff --git a/nixos/modules/hardware/printers.nix b/nixos/modules/hardware/printers.nix
index 85e3215127f..846ff6f3fb4 100644
--- a/nixos/modules/hardware/printers.nix
+++ b/nixos/modules/hardware/printers.nix
@@ -110,21 +110,26 @@ in {
   };
 
   config = mkIf (cfg.ensurePrinters != [] && config.services.printing.enable) {
-    systemd.services.ensure-printers = let
-      cupsUnit = if config.services.printing.startWhenNeeded then "cups.socket" else "cups.service";
-    in {
+    systemd.services.ensure-printers = {
       description = "Ensure NixOS-configured CUPS printers";
       wantedBy = [ "multi-user.target" ];
-      requires = [ cupsUnit ];
-      after = [ cupsUnit ];
+      wants = [ "cups.service" ];
+      after = [ "cups.service" ];
 
       serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
       };
 
-      script = concatMapStringsSep "\n" ensurePrinter cfg.ensurePrinters
-        + optionalString (cfg.ensureDefaultPrinter != null) (ensureDefaultPrinter cfg.ensureDefaultPrinter);
+      script = concatStringsSep "\n" [
+        (concatMapStrings ensurePrinter cfg.ensurePrinters)
+        (optionalString (cfg.ensureDefaultPrinter != null)
+          (ensureDefaultPrinter cfg.ensureDefaultPrinter))
+        # Note: if cupsd is "stateless" the service can't be stopped,
+        # otherwise the configuration will be wiped on the next start.
+        (optionalString (with config.services.printing; startWhenNeeded && !stateless)
+          "systemctl stop cups.service")
+      ];
     };
   };
 }
diff --git a/nixos/modules/hardware/video/hidpi.nix b/nixos/modules/hardware/video/hidpi.nix
deleted file mode 100644
index 8c8f8bc0c26..00000000000
--- a/nixos/modules/hardware/video/hidpi.nix
+++ /dev/null
@@ -1,24 +0,0 @@
-{ lib, pkgs, config, ...}:
-with lib;
-
-{
-  options.hardware.video.hidpi.enable = mkEnableOption (lib.mdDoc "Font/DPI configuration optimized for HiDPI displays");
-
-  config = mkIf config.hardware.video.hidpi.enable {
-    console.font = lib.mkDefault "${pkgs.terminus_font}/share/consolefonts/ter-v32n.psf.gz";
-
-    # Needed when typing in passwords for full disk encryption
-    console.earlySetup = mkDefault true;
-    boot.loader.systemd-boot.consoleMode = mkDefault "1";
-
-
-    # Grayscale anti-aliasing for fonts
-    fonts.fontconfig.antialias = mkDefault true;
-    fonts.fontconfig.subpixel = {
-      rgba = mkDefault "none";
-      lcdfilter = mkDefault "none";
-    };
-
-    # TODO Find reasonable defaults X11 & wayland
-  };
-}
diff --git a/nixos/modules/hardware/video/nvidia.nix b/nixos/modules/hardware/video/nvidia.nix
index cee230ac41c..592d11d6476 100644
--- a/nixos/modules/hardware/video/nvidia.nix
+++ b/nixos/modules/hardware/video/nvidia.nix
@@ -21,17 +21,21 @@ let
   pCfg = cfg.prime;
   syncCfg = pCfg.sync;
   offloadCfg = pCfg.offload;
-  primeEnabled = syncCfg.enable || offloadCfg.enable;
+  reverseSyncCfg = pCfg.reverseSync;
+  primeEnabled = syncCfg.enable || reverseSyncCfg.enable || offloadCfg.enable;
   nvidiaPersistencedEnabled =  cfg.nvidiaPersistenced;
   nvidiaSettings = cfg.nvidiaSettings;
   busIDType = types.strMatching "([[:print:]]+[\:\@][0-9]{1,3}\:[0-9]{1,2}\:[0-9])?";
+
+  ibtSupport = cfg.open || (nvidia_x11.ibtSupport or false);
 in
 
 {
   imports =
     [
       (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "enable" ] [ "hardware" "nvidia" "prime" "sync" "enable" ])
-      (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "allowExternalGpu" ] [ "hardware" "nvidia" "prime" "sync" "allowExternalGpu" ])
+      (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "allowExternalGpu" ] [ "hardware" "nvidia" "prime" "allowExternalGpu" ])
+      (mkRenamedOptionModule [ "hardware" "nvidia" "prime" "sync" "allowExternalGpu" ] [ "hardware" "nvidia" "prime" "allowExternalGpu" ])
       (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "nvidiaBusId" ] [ "hardware" "nvidia" "prime" "nvidiaBusId" ])
       (mkRenamedOptionModule [ "hardware" "nvidia" "optimus_prime" "intelBusId" ] [ "hardware" "nvidia" "prime" "intelBusId" ])
     ];
@@ -104,16 +108,17 @@ in
       description = lib.mdDoc ''
         Enable NVIDIA Optimus support using the NVIDIA proprietary driver via PRIME.
         If enabled, the NVIDIA GPU will be always on and used for all rendering,
-        while enabling output to displays attached only to the integrated Intel GPU
-        without a multiplexer.
+        while enabling output to displays attached only to the integrated Intel/AMD
+        GPU without a multiplexer.
 
         Note that this option only has any effect if the "nvidia" driver is specified
         in {option}`services.xserver.videoDrivers`, and it should preferably
         be the only driver there.
 
-        If this is enabled, then the bus IDs of the NVIDIA and Intel GPUs have to be
-        specified ({option}`hardware.nvidia.prime.nvidiaBusId` and
-        {option}`hardware.nvidia.prime.intelBusId`).
+        If this is enabled, then the bus IDs of the NVIDIA and Intel/AMD GPUs have to
+        be specified ({option}`hardware.nvidia.prime.nvidiaBusId` and
+        {option}`hardware.nvidia.prime.intelBusId` or
+        {option}`hardware.nvidia.prime.amdgpuBusId`).
 
         If you enable this, you may want to also enable kernel modesetting for the
         NVIDIA driver ({option}`hardware.nvidia.modesetting.enable`) in order
@@ -125,11 +130,11 @@ in
       '';
     };
 
-    hardware.nvidia.prime.sync.allowExternalGpu = mkOption {
+    hardware.nvidia.prime.allowExternalGpu = mkOption {
       type = types.bool;
       default = false;
       description = lib.mdDoc ''
-        Configure X to allow external NVIDIA GPUs when using optimus.
+        Configure X to allow external NVIDIA GPUs when using Prime [Reverse] sync optimus.
       '';
     };
 
@@ -139,9 +144,54 @@ in
       description = lib.mdDoc ''
         Enable render offload support using the NVIDIA proprietary driver via PRIME.
 
-        If this is enabled, then the bus IDs of the NVIDIA and Intel GPUs have to be
-        specified ({option}`hardware.nvidia.prime.nvidiaBusId` and
-        {option}`hardware.nvidia.prime.intelBusId`).
+        If this is enabled, then the bus IDs of the NVIDIA and Intel/AMD GPUs have to
+        be specified ({option}`hardware.nvidia.prime.nvidiaBusId` and
+        {option}`hardware.nvidia.prime.intelBusId` or
+        {option}`hardware.nvidia.prime.amdgpuBusId`).
+      '';
+    };
+
+    hardware.nvidia.prime.offload.enableOffloadCmd = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Adds a `nvidia-offload` convenience script to {option}`environment.systemPackages`
+        for offloading programs to an nvidia device. To work, should have also enabled
+        {option}`hardware.nvidia.prime.offload.enable` or {option}`hardware.nvidia.prime.reverseSync.enable`.
+
+        Example usage `nvidia-offload sauerbraten_client`.
+      '';
+    };
+
+    hardware.nvidia.prime.reverseSync.enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Warning: This feature is relatively new, depending on your system this might
+        work poorly. AMD support, especially so.
+        See: https://forums.developer.nvidia.com/t/the-all-new-outputsink-feature-aka-reverse-prime/129828
+
+        Enable NVIDIA Optimus support using the NVIDIA proprietary driver via reverse
+        PRIME. If enabled, the Intel/AMD GPU will be used for all rendering, while
+        enabling output to displays attached only to the NVIDIA GPU without a
+        multiplexer.
+
+        Note that this option only has any effect if the "nvidia" driver is specified
+        in {option}`services.xserver.videoDrivers`, and it should preferably
+        be the only driver there.
+
+        If this is enabled, then the bus IDs of the NVIDIA and Intel/AMD GPUs have to
+        be specified ({option}`hardware.nvidia.prime.nvidiaBusId` and
+        {option}`hardware.nvidia.prime.intelBusId` or
+        {option}`hardware.nvidia.prime.amdgpuBusId`).
+
+        If you enable this, you may want to also enable kernel modesetting for the
+        NVIDIA driver ({option}`hardware.nvidia.modesetting.enable`) in order
+        to prevent tearing.
+
+        Note that this configuration will only be successful when a display manager
+        for which the {option}`services.xserver.displayManager.setupCommands`
+        option is supported is used.
       '';
     };
 
@@ -206,6 +256,13 @@ in
       }
 
       {
+        assertion = offloadCfg.enableOffloadCmd -> offloadCfg.enable || reverseSyncCfg.enable;
+        message = ''
+          Offload command requires offloading or reverse prime sync to be enabled.
+        '';
+      }
+
+      {
         assertion = primeEnabled -> pCfg.nvidiaBusId != "" && (pCfg.intelBusId != "" || pCfg.amdgpuBusId != "");
         message = ''
           When NVIDIA PRIME is enabled, the GPU bus IDs must configured.
@@ -218,8 +275,18 @@ in
       }
 
       {
+        assertion = (reverseSyncCfg.enable && pCfg.amdgpuBusId != "") -> versionAtLeast nvidia_x11.version "470.0";
+        message = "NVIDIA PRIME render offload for AMD APUs is currently only supported on versions >= 470 beta.";
+      }
+
+      {
         assertion = !(syncCfg.enable && offloadCfg.enable);
-        message = "Only one NVIDIA PRIME solution may be used at a time.";
+        message = "PRIME Sync and Offload cannot be both enabled";
+      }
+
+      {
+        assertion = !(syncCfg.enable && reverseSyncCfg.enable);
+        message = "PRIME Sync and PRIME Reverse Sync cannot be both enabled";
       }
 
       {
@@ -257,8 +324,10 @@ in
     # - Configure the display manager to run specific `xrandr` commands which will
     #   configure/enable displays connected to the Intel iGPU / AMD APU.
 
-    services.xserver.drivers = let
-    in optional primeEnabled {
+    # reverse sync implies offloading
+    hardware.nvidia.prime.offload.enable = mkDefault reverseSyncCfg.enable;
+
+    services.xserver.drivers = optional primeEnabled {
       name = igpuDriver;
       display = offloadCfg.enable;
       modules = optionals (igpuDriver == "amdgpu") [ pkgs.xorg.xf86videoamdgpu ];
@@ -273,7 +342,7 @@ in
       deviceSection = optionalString primeEnabled
         ''
           BusID "${pCfg.nvidiaBusId}"
-          ${optionalString syncCfg.allowExternalGpu "Option \"AllowExternalGpus\""}
+          ${optionalString pCfg.allowExternalGpu "Option \"AllowExternalGpus\""}
         '';
       screenSection =
         ''
@@ -290,19 +359,22 @@ in
 
     services.xserver.serverLayoutSection = optionalString syncCfg.enable ''
       Inactive "Device-${igpuDriver}[0]"
+    '' + optionalString reverseSyncCfg.enable ''
+      Inactive "Device-nvidia[0]"
     '' + optionalString offloadCfg.enable ''
       Option "AllowNVIDIAGPUScreens"
     '';
 
     services.xserver.displayManager.setupCommands = let
-      sinkGpuProviderName = if igpuDriver == "amdgpu" then
+      gpuProviderName = 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 ''
+      providerCmdParams = if syncCfg.enable then "\"${gpuProviderName}\" NVIDIA-0" else "NVIDIA-G0 \"${gpuProviderName}\"";
+    in optionalString (syncCfg.enable || reverseSyncCfg.enable) ''
       # Added by nvidia configuration module for Optimus/PRIME.
-      ${pkgs.xorg.xrandr}/bin/xrandr --setprovideroutputsource "${sinkGpuProviderName}" NVIDIA-0
+      ${pkgs.xorg.xrandr}/bin/xrandr --setprovideroutputsource ${providerCmdParams}
       ${pkgs.xorg.xrandr}/bin/xrandr --auto
     '';
 
@@ -325,7 +397,16 @@ in
 
     environment.systemPackages = [ nvidia_x11.bin ]
       ++ optionals cfg.nvidiaSettings [ nvidia_x11.settings ]
-      ++ optionals nvidiaPersistencedEnabled [ nvidia_x11.persistenced ];
+      ++ optionals nvidiaPersistencedEnabled [ nvidia_x11.persistenced ]
+      ++ optionals offloadCfg.enableOffloadCmd [
+        (pkgs.writeShellScriptBin "nvidia-offload" ''
+          export __NV_PRIME_RENDER_OFFLOAD=1
+          export __NV_PRIME_RENDER_OFFLOAD_PROVIDER=NVIDIA-G0
+          export __GLX_VENDOR_LIBRARY_NAME=nvidia
+          export __VK_LAYER_NV_optimus=NVIDIA_only
+          exec "$@"
+        '')
+      ];
 
     systemd.packages = optional cfg.powerManagement.enable nvidia_x11.out;
 
@@ -382,7 +463,8 @@ 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.open "nvidia.NVreg_OpenRmEnableUnsupportedGpus=1";
+      ++ optional cfg.open "nvidia.NVreg_OpenRmEnableUnsupportedGpus=1"
+      ++ optional (config.boot.kernelPackages.kernel.kernelAtLeast "6.2" && !ibtSupport) "ibt=off";
 
     services.udev.extraRules =
       ''
diff --git a/nixos/modules/hardware/video/webcam/ipu6.nix b/nixos/modules/hardware/video/webcam/ipu6.nix
new file mode 100644
index 00000000000..fce78cda34c
--- /dev/null
+++ b/nixos/modules/hardware/video/webcam/ipu6.nix
@@ -0,0 +1,57 @@
+{ config, lib, pkgs, ... }:
+let
+
+  inherit (lib) mkDefault mkEnableOption mkIf mkOption optional types;
+
+  cfg = config.hardware.ipu6;
+
+in
+{
+
+  options.hardware.ipu6 = {
+
+    enable = mkEnableOption (lib.mdDoc "support for Intel IPU6/MIPI cameras");
+
+    platform = mkOption {
+      type = types.enum [ "ipu6" "ipu6ep" ];
+      description = lib.mdDoc ''
+        Choose the version for your hardware platform.
+
+        Use `ipu6` for Tiger Lake and `ipu6ep` for Alder Lake respectively.
+      '';
+    };
+
+  };
+
+  config = mkIf cfg.enable {
+
+    boot.extraModulePackages = with config.boot.kernelPackages; [
+      ipu6-drivers
+    ];
+
+    hardware.firmware = with pkgs; [ ]
+      ++ optional (cfg.platform == "ipu6") ipu6-camera-bin
+      ++ optional (cfg.platform == "ipu6ep") ipu6ep-camera-bin;
+
+    services.udev.extraRules = ''
+      SUBSYSTEM=="intel-ipu6-psys", MODE="0660", GROUP="video"
+    '';
+
+    services.v4l2-relayd.instances.ipu6 = {
+      enable = mkDefault true;
+
+      cardLabel = mkDefault "Intel MIPI Camera";
+
+      extraPackages = with pkgs.gst_all_1; [ ]
+        ++ optional (cfg.platform == "ipu6") icamerasrc-ipu6
+        ++ optional (cfg.platform == "ipu6ep") icamerasrc-ipu6ep;
+
+      input = {
+        pipeline = "icamerasrc";
+        format = mkIf (cfg.platform == "ipu6ep") (mkDefault "NV12");
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/i18n/input-method/default.md b/nixos/modules/i18n/input-method/default.md
new file mode 100644
index 00000000000..42cb8a8d7b6
--- /dev/null
+++ b/nixos/modules/i18n/input-method/default.md
@@ -0,0 +1,160 @@
+# Input Methods {#module-services-input-methods}
+
+Input methods are an operating system component that allows any data, such as
+keyboard strokes or mouse movements, to be received as input. In this way
+users can enter characters and symbols not found on their input devices.
+Using an input method is obligatory for any language that has more graphemes
+than there are keys on the keyboard.
+
+The following input methods are available in NixOS:
+
+  - IBus: The intelligent input bus.
+  - Fcitx5: The next generation of fcitx, addons (including engines, dictionaries, skins) can be added using `i18n.inputMethod.fcitx5.addons`.
+  - Nabi: A Korean input method based on XIM.
+  - Uim: The universal input method, is a library with a XIM bridge.
+  - Hime: An extremely easy-to-use input method framework.
+  - Kime: Korean IME
+
+## IBus {#module-services-input-methods-ibus}
+
+IBus is an Intelligent Input Bus. It provides full featured and user
+friendly input method user interface.
+
+The following snippet can be used to configure IBus:
+
+```
+i18n.inputMethod = {
+  enabled = "ibus";
+  ibus.engines = with pkgs.ibus-engines; [ anthy hangul mozc ];
+};
+```
+
+`i18n.inputMethod.ibus.engines` is optional and can be used
+to add extra IBus engines.
+
+Available extra IBus engines are:
+
+  - Anthy (`ibus-engines.anthy`): Anthy is a system for
+    Japanese input method. It converts Hiragana text to Kana Kanji mixed text.
+  - Hangul (`ibus-engines.hangul`): Korean input method.
+  - m17n (`ibus-engines.m17n`): m17n is an input method that
+    uses input methods and corresponding icons in the m17n database.
+  - mozc (`ibus-engines.mozc`): A Japanese input method from
+    Google.
+  - Table (`ibus-engines.table`): An input method that load
+    tables of input methods.
+  - table-others (`ibus-engines.table-others`): Various
+    table-based input methods. To use this, and any other table-based input
+    methods, it must appear in the list of engines along with
+    `table`. For example:
+
+    ```
+    ibus.engines = with pkgs.ibus-engines; [ table table-others ];
+    ```
+
+To use any input method, the package must be added in the configuration, as
+shown above, and also (after running `nixos-rebuild`) the
+input method must be added from IBus' preference dialog.
+
+### Troubleshooting {#module-services-input-methods-troubleshooting}
+
+If IBus works in some applications but not others, a likely cause of this
+is that IBus is depending on a different version of `glib`
+to what the applications are depending on. This can be checked by running
+`nix-store -q --requisites <path> | grep glib`,
+where `<path>` is the path of either IBus or an
+application in the Nix store. The `glib` packages must
+match exactly. If they do not, uninstalling and reinstalling the
+application is a likely fix.
+
+## Fcitx5 {#module-services-input-methods-fcitx}
+
+Fcitx5 is an input method framework with extension support. It has three
+built-in Input Method Engine, Pinyin, QuWei and Table-based input methods.
+
+The following snippet can be used to configure Fcitx:
+
+```
+i18n.inputMethod = {
+  enabled = "fcitx5";
+  fcitx5.addons = with pkgs; [ fcitx5-mozc fcitx5-hangul fcitx5-m17n ];
+};
+```
+
+`i18n.inputMethod.fcitx5.addons` is optional and can be
+used to add extra Fcitx5 addons.
+
+Available extra Fcitx5 addons are:
+
+  - Anthy (`fcitx5-anthy`): Anthy is a system for
+    Japanese input method. It converts Hiragana text to Kana Kanji mixed text.
+  - Chewing (`fcitx5-chewing`): Chewing is an
+    intelligent Zhuyin input method. It is one of the most popular input
+    methods among Traditional Chinese Unix users.
+  - Hangul (`fcitx5-hangul`): Korean input method.
+  - Unikey (`fcitx5-unikey`): Vietnamese input method.
+  - m17n (`fcitx5-m17n`): m17n is an input method that
+    uses input methods and corresponding icons in the m17n database.
+  - mozc (`fcitx5-mozc`): A Japanese input method from
+    Google.
+  - table-others (`fcitx5-table-other`): Various
+    table-based input methods.
+  - chinese-addons (`fcitx5-chinese-addons`): Various chinese input methods.
+  - rime (`fcitx5-rime`): RIME support for fcitx5.
+
+## Nabi {#module-services-input-methods-nabi}
+
+Nabi is an easy to use Korean X input method. It allows you to enter
+phonetic Korean characters (hangul) and pictographic Korean characters
+(hanja).
+
+The following snippet can be used to configure Nabi:
+
+```
+i18n.inputMethod = {
+  enabled = "nabi";
+};
+```
+
+## Uim {#module-services-input-methods-uim}
+
+Uim (short for "universal input method") is a multilingual input method
+framework. Applications can use it through so-called bridges.
+
+The following snippet can be used to configure uim:
+
+```
+i18n.inputMethod = {
+  enabled = "uim";
+};
+```
+
+Note: The [](#opt-i18n.inputMethod.uim.toolbar) option can be
+used to choose uim toolbar.
+
+## Hime {#module-services-input-methods-hime}
+
+Hime is an extremely easy-to-use input method framework. It is lightweight,
+stable, powerful and supports many commonly used input methods, including
+Cangjie, Zhuyin, Dayi, Rank, Shrimp, Greek, Korean Pinyin, Latin Alphabet,
+etc...
+
+The following snippet can be used to configure Hime:
+
+```
+i18n.inputMethod = {
+  enabled = "hime";
+};
+```
+
+## Kime {#module-services-input-methods-kime}
+
+Kime is Korean IME. it's built with Rust language and let you get simple, safe, fast Korean typing
+
+The following snippet can be used to configure Kime:
+
+```
+i18n.inputMethod = {
+  enabled = "kime";
+};
+```
diff --git a/nixos/modules/i18n/input-method/default.nix b/nixos/modules/i18n/input-method/default.nix
index 07fb86bcc25..d967d4335c7 100644
--- a/nixos/modules/i18n/input-method/default.nix
+++ b/nixos/modules/i18n/input-method/default.nix
@@ -29,9 +29,9 @@ in
   options.i18n = {
     inputMethod = {
       enabled = mkOption {
-        type    = types.nullOr (types.enum [ "ibus" "fcitx" "fcitx5" "nabi" "uim" "hime" "kime" ]);
+        type    = types.nullOr (types.enum [ "ibus" "fcitx5" "nabi" "uim" "hime" "kime" ]);
         default = null;
-        example = "fcitx";
+        example = "fcitx5";
         description = lib.mdDoc ''
           Select the enabled input method. Input methods is a software to input symbols that are not available on standard input devices.
 
@@ -40,7 +40,6 @@ in
           Currently the following input methods are available in NixOS:
 
           - ibus: The intelligent input bus, extra input engines can be added using `i18n.inputMethod.ibus.engines`.
-          - fcitx: A customizable lightweight input method, extra input engines can be added using `i18n.inputMethod.fcitx.engines`.
           - fcitx5: The next generation of fcitx, addons (including engines, dictionaries, skins) can be added using `i18n.inputMethod.fcitx5.addons`.
           - nabi: A Korean input method based on XIM. Nabi doesn't support Qt 5.
           - uim: The universal input method, is a library with a XIM bridge. uim mainly support Chinese, Japanese and Korean.
@@ -66,7 +65,7 @@ in
 
   meta = {
     maintainers = with lib.maintainers; [ ericsagnes ];
-    doc = ./default.xml;
+    doc = ./default.md;
   };
 
 }
diff --git a/nixos/modules/i18n/input-method/default.xml b/nixos/modules/i18n/input-method/default.xml
deleted file mode 100644
index dd66316c730..00000000000
--- a/nixos/modules/i18n/input-method/default.xml
+++ /dev/null
@@ -1,291 +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-input-methods">
- <title>Input Methods</title>
- <para>
-  Input methods are an operating system component that allows any data, such as
-  keyboard strokes or mouse movements, to be received as input. In this way
-  users can enter characters and symbols not found on their input devices.
-  Using an input method is obligatory for any language that has more graphemes
-  than there are keys on the keyboard.
- </para>
- <para>
-  The following input methods are available in NixOS:
- </para>
- <itemizedlist>
-  <listitem>
-   <para>
-    IBus: The intelligent input bus.
-   </para>
-  </listitem>
-  <listitem>
-   <para>
-    Fcitx: A customizable lightweight input method.
-   </para>
-  </listitem>
-  <listitem>
-   <para>
-    Nabi: A Korean input method based on XIM.
-   </para>
-  </listitem>
-  <listitem>
-   <para>
-    Uim: The universal input method, is a library with a XIM bridge.
-   </para>
-  </listitem>
-  <listitem>
-   <para>
-    Hime: An extremely easy-to-use input method framework.
-   </para>
-  </listitem>
-  <listitem>
-    <para>
-     Kime: Korean IME
-    </para>
-  </listitem>
- </itemizedlist>
- <section xml:id="module-services-input-methods-ibus">
-  <title>IBus</title>
-
-  <para>
-   IBus is an Intelligent Input Bus. It provides full featured and user
-   friendly input method user interface.
-  </para>
-
-  <para>
-   The following snippet can be used to configure IBus:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "ibus";
-  <link linkend="opt-i18n.inputMethod.ibus.engines">ibus.engines</link> = with pkgs.ibus-engines; [ anthy hangul mozc ];
-};
-</programlisting>
-
-  <para>
-   <literal>i18n.inputMethod.ibus.engines</literal> is optional and can be used
-   to add extra IBus engines.
-  </para>
-
-  <para>
-   Available extra IBus engines are:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Anthy (<literal>ibus-engines.anthy</literal>): Anthy is a system for
-     Japanese input method. It converts Hiragana text to Kana Kanji mixed text.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Hangul (<literal>ibus-engines.hangul</literal>): Korean input method.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     m17n (<literal>ibus-engines.m17n</literal>): m17n is an input method that
-     uses input methods and corresponding icons in the m17n database.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     mozc (<literal>ibus-engines.mozc</literal>): A Japanese input method from
-     Google.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Table (<literal>ibus-engines.table</literal>): An input method that load
-     tables of input methods.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     table-others (<literal>ibus-engines.table-others</literal>): Various
-     table-based input methods. To use this, and any other table-based input
-     methods, it must appear in the list of engines along with
-     <literal>table</literal>. For example:
-<programlisting>
-ibus.engines = with pkgs.ibus-engines; [ table table-others ];
-</programlisting>
-    </para>
-   </listitem>
-  </itemizedlist>
-
-  <para>
-   To use any input method, the package must be added in the configuration, as
-   shown above, and also (after running <literal>nixos-rebuild</literal>) the
-   input method must be added from IBus' preference dialog.
-  </para>
-
-  <simplesect xml:id="module-services-input-methods-troubleshooting">
-   <title>Troubleshooting</title>
-   <para>
-    If IBus works in some applications but not others, a likely cause of this
-    is that IBus is depending on a different version of <literal>glib</literal>
-    to what the applications are depending on. This can be checked by running
-    <literal>nix-store -q --requisites &lt;path&gt; | grep glib</literal>,
-    where <literal>&lt;path&gt;</literal> is the path of either IBus or an
-    application in the Nix store. The <literal>glib</literal> packages must
-    match exactly. If they do not, uninstalling and reinstalling the
-    application is a likely fix.
-   </para>
-  </simplesect>
- </section>
- <section xml:id="module-services-input-methods-fcitx">
-  <title>Fcitx</title>
-
-  <para>
-   Fcitx is an input method framework with extension support. It has three
-   built-in Input Method Engine, Pinyin, QuWei and Table-based input methods.
-  </para>
-
-  <para>
-   The following snippet can be used to configure Fcitx:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "fcitx";
-  <link linkend="opt-i18n.inputMethod.fcitx.engines">fcitx.engines</link> = with pkgs.fcitx-engines; [ mozc hangul m17n ];
-};
-</programlisting>
-
-  <para>
-   <literal>i18n.inputMethod.fcitx.engines</literal> is optional and can be
-   used to add extra Fcitx engines.
-  </para>
-
-  <para>
-   Available extra Fcitx engines are:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Anthy (<literal>fcitx-engines.anthy</literal>): Anthy is a system for
-     Japanese input method. It converts Hiragana text to Kana Kanji mixed text.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Chewing (<literal>fcitx-engines.chewing</literal>): Chewing is an
-     intelligent Zhuyin input method. It is one of the most popular input
-     methods among Traditional Chinese Unix users.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Hangul (<literal>fcitx-engines.hangul</literal>): Korean input method.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Unikey (<literal>fcitx-engines.unikey</literal>): Vietnamese input method.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     m17n (<literal>fcitx-engines.m17n</literal>): m17n is an input method that
-     uses input methods and corresponding icons in the m17n database.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     mozc (<literal>fcitx-engines.mozc</literal>): A Japanese input method from
-     Google.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     table-others (<literal>fcitx-engines.table-others</literal>): Various
-     table-based input methods.
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
- <section xml:id="module-services-input-methods-nabi">
-  <title>Nabi</title>
-
-  <para>
-   Nabi is an easy to use Korean X input method. It allows you to enter
-   phonetic Korean characters (hangul) and pictographic Korean characters
-   (hanja).
-  </para>
-
-  <para>
-   The following snippet can be used to configure Nabi:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "nabi";
-};
-</programlisting>
- </section>
- <section xml:id="module-services-input-methods-uim">
-  <title>Uim</title>
-
-  <para>
-   Uim (short for "universal input method") is a multilingual input method
-   framework. Applications can use it through so-called bridges.
-  </para>
-
-  <para>
-   The following snippet can be used to configure uim:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "uim";
-};
-</programlisting>
-
-  <para>
-   Note: The <xref linkend="opt-i18n.inputMethod.uim.toolbar"/> option can be
-   used to choose uim toolbar.
-  </para>
- </section>
- <section xml:id="module-services-input-methods-hime">
-  <title>Hime</title>
-
-  <para>
-   Hime is an extremely easy-to-use input method framework. It is lightweight,
-   stable, powerful and supports many commonly used input methods, including
-   Cangjie, Zhuyin, Dayi, Rank, Shrimp, Greek, Korean Pinyin, Latin Alphabet,
-   etc...
-  </para>
-
-  <para>
-   The following snippet can be used to configure Hime:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "hime";
-};
-</programlisting>
- </section>
- <section xml:id="module-services-input-methods-kime">
-  <title>Kime</title>
-
-  <para>
-   Kime is Korean IME. it's built with Rust language and let you get simple, safe, fast Korean typing
-  </para>
-
-  <para>
-   The following snippet can be used to configure Kime:
-  </para>
-
-<programlisting>
-i18n.inputMethod = {
-  <link linkend="opt-i18n.inputMethod.enabled">enabled</link> = "kime";
-};
-</programlisting>
- </section>
-</chapter>
diff --git a/nixos/modules/i18n/input-method/fcitx.nix b/nixos/modules/i18n/input-method/fcitx.nix
deleted file mode 100644
index 043ec3d55c1..00000000000
--- a/nixos/modules/i18n/input-method/fcitx.nix
+++ /dev/null
@@ -1,46 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-let
-  cfg = config.i18n.inputMethod.fcitx;
-  fcitxPackage = pkgs.fcitx.override { plugins = cfg.engines; };
-  fcitxEngine = types.package // {
-    name  = "fcitx-engine";
-    check = x: (lib.types.package.check x) && (attrByPath ["meta" "isFcitxEngine"] false x);
-  };
-in
-{
-  options = {
-
-    i18n.inputMethod.fcitx = {
-      engines = mkOption {
-        type    = with types; listOf fcitxEngine;
-        default = [];
-        example = literalExpression "with pkgs.fcitx-engines; [ mozc hangul ]";
-        description =
-          let
-            enginesDrv = filterAttrs (const isDerivation) pkgs.fcitx-engines;
-            engines = concatStringsSep ", "
-              (map (name: "`${name}`") (attrNames enginesDrv));
-          in
-            lib.mdDoc "Enabled Fcitx engines. Available engines are: ${engines}.";
-      };
-    };
-
-  };
-
-  config = mkIf (config.i18n.inputMethod.enabled == "fcitx") {
-    i18n.inputMethod.package = fcitxPackage;
-
-    environment.variables = {
-      GTK_IM_MODULE = "fcitx";
-      QT_IM_MODULE  = "fcitx";
-      XMODIFIERS    = "@im=fcitx";
-    };
-    services.xserver.displayManager.sessionCommands = "${fcitxPackage}/bin/fcitx";
-  };
-
-  # uses attributes of the linked package
-  meta.buildDocsInSandbox = false;
-}
diff --git a/nixos/modules/i18n/input-method/fcitx5.nix b/nixos/modules/i18n/input-method/fcitx5.nix
index aa816c90a3d..7251240d26a 100644
--- a/nixos/modules/i18n/input-method/fcitx5.nix
+++ b/nixos/modules/i18n/input-method/fcitx5.nix
@@ -5,10 +5,9 @@ with lib;
 let
   im = config.i18n.inputMethod;
   cfg = im.fcitx5;
-  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 {
+  fcitx5Package = pkgs.fcitx5-with-addons.override { inherit (cfg) addons; };
+in
+{
   options = {
     i18n.inputMethod.fcitx5 = {
       addons = mkOption {
@@ -19,30 +18,23 @@ in {
           Enabled Fcitx5 addons.
         '';
       };
-
-      enableRimeData = mkEnableOption (lib.mdDoc "default rime-data with fcitx5-rime");
     };
   };
 
+  imports = [
+    (mkRemovedOptionModule [ "i18n" "inputMethod" "fcitx5" "enableRimeData" ] ''
+      RIME data is now included in `fcitx5-rime` by default, and can be customized using `fcitx5-rime.override { rimeDataPkgs = ...; }`
+    '')
+  ];
+
   config = mkIf (im.enabled == "fcitx5") {
     i18n.inputMethod.package = fcitx5Package;
 
-    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"
-      ];
-
-      variables =  {
-        NIX_RIME_DATA_DIR = "/run/current-system/sw/share/rime-data";
-      };
-    })];
+    environment.variables = {
+      GTK_IM_MODULE = "fcitx";
+      QT_IM_MODULE = "fcitx";
+      XMODIFIERS = "@im=fcitx";
+      QT_PLUGIN_PATH = [ "${fcitx5Package}/${pkgs.qt6.qtbase.qtPluginPrefix}" ];
+    };
   };
 }
diff --git a/nixos/modules/i18n/input-method/ibus.nix b/nixos/modules/i18n/input-method/ibus.nix
index 520db128acd..2a35afad2ac 100644
--- a/nixos/modules/i18n/input-method/ibus.nix
+++ b/nixos/modules/i18n/input-method/ibus.nix
@@ -10,10 +10,7 @@ let
     check = x: (lib.types.package.check x) && (attrByPath ["meta" "isIbusEngine"] false x);
   };
 
-  impanel =
-    if cfg.panel != null
-    then "--panel=${cfg.panel}"
-    else "";
+  impanel = optionalString (cfg.panel != null) "--panel=${cfg.panel}";
 
   ibusAutostart = pkgs.writeTextFile {
     name = "autostart-ibus-daemon";
diff --git a/nixos/modules/i18n/input-method/kime.nix b/nixos/modules/i18n/input-method/kime.nix
index 29224a6bf75..e82996926b2 100644
--- a/nixos/modules/i18n/input-method/kime.nix
+++ b/nixos/modules/i18n/input-method/kime.nix
@@ -1,40 +1,37 @@
 { config, pkgs, lib, generators, ... }:
-with lib;
-let
-  cfg = config.i18n.inputMethod.kime;
-  yamlFormat = pkgs.formats.yaml { };
-in
-{
-  options = {
-    i18n.inputMethod.kime = {
-      config = mkOption {
-        type = yamlFormat.type;
-        default = { };
-        example = literalExpression ''
-          {
-            daemon = {
-              modules = ["Xim" "Indicator"];
-            };
+let imcfg = config.i18n.inputMethod;
+in {
+  imports = [
+    (lib.mkRemovedOptionModule [ "i18n" "inputMethod" "kime" "config" ] "Use i18n.inputMethod.kime.* instead")
+  ];
 
-            indicator = {
-              icon_color = "White";
-            };
-
-            engine = {
-              hangul = {
-                layout = "dubeolsik";
-              };
-            };
-          }
-          '';
-        description = lib.mdDoc ''
-          kime configuration. Refer to <https://github.com/Riey/kime/blob/v${pkgs.kime.version}/docs/CONFIGURATION.md> for details on supported values.
-        '';
-      };
+  options.i18n.inputMethod.kime = {
+    daemonModules = lib.mkOption {
+      type = lib.types.listOf (lib.types.enum [ "Xim" "Wayland" "Indicator" ]);
+      default = [ "Xim" "Wayland" "Indicator" ];
+      example = [ "Xim" "Indicator" ];
+      description = lib.mdDoc ''
+        List of enabled daemon modules
+      '';
+    };
+    iconColor = lib.mkOption {
+      type = lib.types.enum [ "Black" "White" ];
+      default = "Black";
+      example = "White";
+      description = lib.mdDoc ''
+        Color of the indicator icon
+      '';
+    };
+    extraConfig = lib.mkOption {
+      type = lib.types.lines;
+      default = "";
+      description = lib.mdDoc ''
+        extra kime configuration. Refer to <https://github.com/Riey/kime/blob/v${pkgs.kime.version}/docs/CONFIGURATION.md> for details on supported values.
+      '';
     };
   };
 
-  config = mkIf (config.i18n.inputMethod.enabled == "kime") {
+  config = lib.mkIf (imcfg.enabled == "kime") {
     i18n.inputMethod.package = pkgs.kime;
 
     environment.variables = {
@@ -43,7 +40,12 @@ in
       XMODIFIERS    = "@im=kime";
     };
 
-    environment.etc."xdg/kime/config.yaml".text = replaceStrings [ "\\\\" ] [ "\\" ] (builtins.toJSON cfg.config);
+    environment.etc."xdg/kime/config.yaml".text = ''
+      daemon:
+        modules: [${lib.concatStringsSep "," imcfg.kime.daemonModules}]
+      indicator:
+        icon_color: ${imcfg.kime.iconColor}
+    '' + imcfg.kime.extraConfig;
   };
 
   # uses attributes of the linked package
diff --git a/nixos/modules/installer/cd-dvd/channel.nix b/nixos/modules/installer/cd-dvd/channel.nix
index 4b4c2e39334..8426ba8fac0 100644
--- a/nixos/modules/installer/cd-dvd/channel.nix
+++ b/nixos/modules/installer/cd-dvd/channel.nix
@@ -42,7 +42,7 @@ in
   # see discussion in https://github.com/NixOS/nixpkgs/pull/204178#issuecomment-1336289021
   nix.registry.nixpkgs.to = {
     type = "path";
-    path = nixpkgs;
+    path = "${channelSources}/nixos";
   };
 
   # Provide the NixOS/Nixpkgs sources in /etc/nixos.  This is required
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-base.nix b/nixos/modules/installer/cd-dvd/installation-cd-base.nix
index 3f92b779d60..3c7c7e30a0b 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-base.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-base.nix
@@ -21,6 +21,9 @@ with lib;
   # ISO naming.
   isoImage.isoName = "${config.isoImage.isoBaseName}-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}.iso";
 
+  # BIOS booting
+  isoImage.makeBiosBootable = true;
+
   # EFI booting
   isoImage.makeEfiBootable = true;
 
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
index d015e10c11d..12feb2d96ec 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-gnome.nix
@@ -31,7 +31,7 @@
   };
 
   # Theme calamares with GNOME theme
-  qt5 = {
+  qt = {
     enable = true;
     platformTheme = "gnome";
   };
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel-no-zfs.nix b/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel-no-zfs.nix
new file mode 100644
index 00000000000..9d09cdbe020
--- /dev/null
+++ b/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel-no-zfs.nix
@@ -0,0 +1,15 @@
+{ pkgs, ... }:
+
+{
+  imports = [ ./installation-cd-minimal-new-kernel.nix ];
+
+  # Makes `availableOn` fail for zfs, see <nixos/modules/profiles/base.nix>.
+  # This is a workaround since we cannot remove the `"zfs"` string from `supportedFilesystems`.
+  # The proper fix would be to make `supportedFilesystems` an attrset with true/false which we
+  # could then `lib.mkForce false`
+  nixpkgs.overlays = [(final: super: {
+    zfs = super.zfs.overrideAttrs(_: {
+      meta.platforms = [];
+    });
+  })];
+}
diff --git a/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix b/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix
index abf0a5186b6..29afdd47109 100644
--- a/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix
+++ b/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix
@@ -9,8 +9,15 @@
     ./installation-cd-base.nix
   ];
 
+  # Causes a lot of uncached builds for a negligible decrease in size.
+  environment.noXlibs = lib.mkOverride 500 false;
+
   documentation.man.enable = lib.mkOverride 500 true;
 
+  # Although we don't really need HTML documentation in the minimal installer,
+  # not including it may cause annoying cache misses in the case of the NixOS manual.
+  documentation.doc.enable = lib.mkOverride 500 true;
+
   fonts.fontconfig.enable = lib.mkForce false;
 
   isoImage.edition = lib.mkForce "minimal";
diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix
index 5bd343c85fa..fcdee1bc096 100644
--- a/nixos/modules/installer/cd-dvd/iso-image.nix
+++ b/nixos/modules/installer/cd-dvd/iso-image.nix
@@ -22,8 +22,8 @@ let
       (option: ''
         menuentry '${defaults.name} ${
         # Name appended to menuentry defaults to params if no specific name given.
-        option.name or (if option ? params then "(${option.params})" else "")
-        }' ${if option ? class then " --class ${option.class}" else ""} {
+        option.name or (optionalString (option ? params) "(${option.params})")
+        }' ${optionalString (option ? class) " --class ${option.class}"} {
           linux ${defaults.image} \''${isoboot} ${defaults.params} ${
             option.params or ""
           }
@@ -52,7 +52,7 @@ let
   buildMenuAdditionalParamsGrub2 = additional:
   let
     finalCfg = {
-      name = "NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel}";
+      name = "${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel}";
       params = "init=${config.system.build.toplevel}/init ${additional} ${toString config.boot.kernelParams}";
       image = "/boot/${config.system.boot.loader.kernelFile}";
       initrd = "/boot/initrd";
@@ -70,14 +70,20 @@ let
   ;
 
   # Timeout in syslinux is in units of 1/10 of a second.
-  # 0 is used to disable timeouts.
+  # null means max timeout (35996, just under 1h in 1/10 seconds)
+  # 0 means disable timeout
   syslinuxTimeout = if config.boot.loader.timeout == null then
-      0
+      35996
     else
-      max (config.boot.loader.timeout * 10) 1;
+      config.boot.loader.timeout * 10;
 
-
-  max = x: y: if x > y then x else y;
+  # Timeout in grub is in seconds.
+  # null means max timeout (infinity)
+  # 0 means disable timeout
+  grubEfiTimeout = if config.boot.loader.timeout == null then
+      -1
+    else
+      config.boot.loader.timeout;
 
   # The configuration file for syslinux.
 
@@ -103,35 +109,35 @@ let
     DEFAULT boot
 
     LABEL boot
-    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel}
+    MENU LABEL ${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel}
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}
     INITRD /boot/${config.system.boot.loader.initrdFile}
 
     # A variant to boot with 'nomodeset'
     LABEL boot-nomodeset
-    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (nomodeset)
+    MENU LABEL ${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (nomodeset)
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} nomodeset
     INITRD /boot/${config.system.boot.loader.initrdFile}
 
     # A variant to boot with 'copytoram'
     LABEL boot-copytoram
-    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (copytoram)
+    MENU LABEL ${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (copytoram)
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} copytoram
     INITRD /boot/${config.system.boot.loader.initrdFile}
 
     # A variant to boot with verbose logging to the console
     LABEL boot-debug
-    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (debug)
+    MENU LABEL ${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (debug)
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} loglevel=7
     INITRD /boot/${config.system.boot.loader.initrdFile}
 
     # A variant to boot with a serial console enabled
     LABEL boot-serial
-    MENU LABEL NixOS ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (serial console=ttyS0,115200n8)
+    MENU LABEL ${config.system.nixos.distroName} ${config.system.nixos.label}${config.isoImage.appendToMenuLabel} (serial console=ttyS0,115200n8)
     LINUX /boot/${config.system.boot.loader.kernelFile}
     APPEND init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams} console=ttyS0,115200n8
     INITRD /boot/${config.system.boot.loader.initrdFile}
@@ -286,7 +292,7 @@ let
     if serial; then set with_serial=yes ;fi
     export with_serial
     clear
-    set timeout=10
+    set timeout=${toString grubEfiTimeout}
 
     # This message will only be viewable when "gfxterm" is not used.
     echo ""
@@ -452,7 +458,7 @@ in
     };
 
     isoImage.isoBaseName = mkOption {
-      default = "nixos";
+      default = config.system.nixos.distroId;
       description = lib.mdDoc ''
         Prefix of the name of the generated ISO image file.
       '';
@@ -467,7 +473,7 @@ in
     };
 
     isoImage.squashfsCompression = mkOption {
-      default = with pkgs.stdenv.targetPlatform; "xz -Xdict-size 100% "
+      default = with pkgs.stdenv.hostPlatform; "xz -Xdict-size 100% "
                 + lib.optionalString isx86 "-Xbcj x86"
                 # Untested but should also reduce size for these platforms
                 + lib.optionalString isAarch "-Xbcj arm"
@@ -529,10 +535,17 @@ in
       '';
     };
 
+    isoImage.makeBiosBootable = mkOption {
+      default = false;
+      description = lib.mdDoc ''
+        Whether the ISO image should be a BIOS-bootable disk.
+      '';
+    };
+
     isoImage.makeEfiBootable = mkOption {
       default = false;
       description = lib.mdDoc ''
-        Whether the ISO image should be an efi-bootable volume.
+        Whether the ISO image should be an EFI-bootable volume.
       '';
     };
 
@@ -573,7 +586,7 @@ in
 
     isoImage.syslinuxTheme = mkOption {
       default = ''
-        MENU TITLE NixOS
+        MENU TITLE ${config.system.nixos.distroName}
         MENU RESOLUTION 800 600
         MENU CLEAR
         MENU ROWS 6
@@ -687,7 +700,7 @@ in
     boot.loader.grub.enable = false;
 
     environment.systemPackages =  [ grubPkgs.grub2 grubPkgs.grub2_efi ]
-      ++ optional canx86BiosBoot pkgs.syslinux
+      ++ optional (config.isoImage.makeBiosBootable && canx86BiosBoot) pkgs.syslinux
     ;
 
     # In stage 1 of the boot, mount the CD as the root FS by label so
@@ -738,7 +751,7 @@ in
         { source = pkgs.writeText "version" config.system.nixos.label;
           target = "/version.txt";
         }
-      ] ++ optionals canx86BiosBoot [
+      ] ++ optionals (config.isoImage.makeBiosBootable && canx86BiosBoot) [
         { source = config.isoImage.splashImage;
           target = "/isolinux/background.png";
         }
@@ -765,7 +778,7 @@ in
         { source = config.isoImage.efiSplashImage;
           target = "/EFI/boot/efi-background.png";
         }
-      ] ++ optionals (config.boot.loader.grub.memtest86.enable && canx86BiosBoot) [
+      ] ++ optionals (config.boot.loader.grub.memtest86.enable && config.isoImage.makeBiosBootable && canx86BiosBoot) [
         { source = "${pkgs.memtest86plus}/memtest.bin";
           target = "/boot/memtest.bin";
         }
@@ -780,10 +793,10 @@ in
     # Create the ISO image.
     system.build.isoImage = pkgs.callPackage ../../../lib/make-iso9660-image.nix ({
       inherit (config.isoImage) isoName compressImage volumeID contents;
-      bootable = canx86BiosBoot;
+      bootable = config.isoImage.makeBiosBootable && canx86BiosBoot;
       bootImage = "/isolinux/isolinux.bin";
-      syslinux = if canx86BiosBoot then pkgs.syslinux else null;
-    } // optionalAttrs (config.isoImage.makeUsbBootable && canx86BiosBoot) {
+      syslinux = if config.isoImage.makeBiosBootable && canx86BiosBoot then pkgs.syslinux else null;
+    } // optionalAttrs (config.isoImage.makeUsbBootable && config.isoImage.makeBiosBootable && canx86BiosBoot) {
       usbBootable = true;
       isohybridMbrImage = "${pkgs.syslinux}/share/syslinux/isohdpfx.bin";
     } // optionalAttrs config.isoImage.makeEfiBootable {
diff --git a/nixos/modules/installer/netboot/netboot-minimal.nix b/nixos/modules/installer/netboot/netboot-minimal.nix
index 91065d52faf..5ca255acf35 100644
--- a/nixos/modules/installer/netboot/netboot-minimal.nix
+++ b/nixos/modules/installer/netboot/netboot-minimal.nix
@@ -9,4 +9,7 @@
   ];
 
   documentation.man.enable = lib.mkOverride 500 true;
+  hardware.enableRedistributableFirmware = lib.mkOverride 70 false;
+  system.extraDependencies = lib.mkOverride 70 [];
+  networking.wireless.enable = lib.mkOverride 500 false;
 }
diff --git a/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-no-zfs-installer.nix b/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-no-zfs-installer.nix
new file mode 100644
index 00000000000..0e505596029
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-no-zfs-installer.nix
@@ -0,0 +1,15 @@
+{ pkgs, ... }:
+
+{
+  imports = [ ./sd-image-aarch64-new-kernel-installer.nix ];
+
+  # Makes `availableOn` fail for zfs, see <nixos/modules/profiles/base.nix>.
+  # This is a workaround since we cannot remove the `"zfs"` string from `supportedFilesystems`.
+  # The proper fix would be to make `supportedFilesystems` an attrset with true/false which we
+  # could then `lib.mkForce false`
+  nixpkgs.overlays = [(final: super: {
+    zfs = super.zfs.overrideAttrs(_: {
+      meta.platforms = [];
+    });
+  })];
+}
diff --git a/nixos/modules/installer/sd-card/sd-image-powerpc64le.nix b/nixos/modules/installer/sd-card/sd-image-powerpc64le.nix
new file mode 100644
index 00000000000..143c678e43f
--- /dev/null
+++ b/nixos/modules/installer/sd-card/sd-image-powerpc64le.nix
@@ -0,0 +1,49 @@
+# To build, use:
+# nix-build nixos -I nixos-config=nixos/modules/installer/sd-card/sd-image-powerpc64le.nix -A config.system.build.sdImage
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ../../profiles/base.nix
+    ../../profiles/installation-device.nix
+    ./sd-image.nix
+  ];
+
+  boot.loader = {
+    # powerpc64le-linux typically uses petitboot
+    grub.enable = false;
+    generic-extlinux-compatible = {
+      # petitboot is not does not support all of the extlinux extensions to
+      # syslinux, but its parser is very forgiving; it essentially ignores
+      # whatever it doesn't understand.  See below for a filename adjustment.
+      enable = true;
+    };
+  };
+
+  boot.consoleLogLevel = lib.mkDefault 7;
+  boot.kernelParams = [ "console=hvc0" ];
+
+  sdImage = {
+    populateFirmwareCommands = "";
+    populateRootCommands = ''
+      mkdir -p ./files/boot
+      ${config.boot.loader.generic-extlinux-compatible.populateCmd} \
+        -c ${config.system.build.toplevel} \
+        -d ./files/boot
+    ''
+    # https://github.com/open-power/petitboot/blob/master/discover/syslinux-parser.c
+    # petitboot will look in these paths (plus all-caps versions of them):
+    #  /boot/syslinux/syslinux.cfg
+    #  /syslinux/syslinux.cfg
+    #  /syslinux.cfg
+    + ''
+      mv ./files/boot/extlinux ./files/boot/syslinux
+      mv ./files/boot/syslinux/extlinux.conf ./files/boot/syslinux/syslinux.cfg
+    ''
+    # petitboot does not support relative paths for LINUX or INITRD; it prepends
+    # a `/` when parsing these fields
+    + ''
+      sed -i 's_^\(\W\W*\(INITRD\|initrd\|LINUX\|linux\)\W\)\.\./_\1/boot/_' ./files/boot/syslinux/syslinux.cfg
+    '';
+  };
+}
diff --git a/nixos/modules/installer/tools/nix-fallback-paths.nix b/nixos/modules/installer/tools/nix-fallback-paths.nix
index 3eca901bdbf..1058a34133b 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/h88w1442c7hzkbw8sgpcsbqp4lhz6l5p-nix-2.12.0";
-  i686-linux = "/nix/store/j23527l1c3hfx17nssc0v53sq6c741zs-nix-2.12.0";
-  aarch64-linux = "/nix/store/zgzmdymyh934y3r4vqh8z337ba4cwsjb-nix-2.12.0";
-  x86_64-darwin = "/nix/store/wnlrzllazdyg1nrw9na497p4w0m7i7mm-nix-2.12.0";
-  aarch64-darwin = "/nix/store/7n5yamgzg5dpp5vb6ipdqgfh6cf30wmn-nix-2.12.0";
+  x86_64-linux = "/nix/store/mc43d38fibi94pp5crfwacl5gbslccd0-nix-2.13.3";
+  i686-linux = "/nix/store/09m966pj26cgd4ihlg8ihl1106j3vih8-nix-2.13.3";
+  aarch64-linux = "/nix/store/7f191d125akld27gc6jl0r13l8pl7x0h-nix-2.13.3";
+  x86_64-darwin = "/nix/store/1wn9jkvi2zqfjnjgg7lnp30r2q2y8whd-nix-2.13.3";
+  aarch64-darwin = "/nix/store/8w0v2mffa10chrf1h66cbvbpw86qmh85-nix-2.13.3";
 }
diff --git a/nixos/modules/installer/tools/nixos-enter.sh b/nixos/modules/installer/tools/nixos-enter.sh
index 89beeee7cf9..60a86d89abb 100644..100755
--- a/nixos/modules/installer/tools/nixos-enter.sh
+++ b/nixos/modules/installer/tools/nixos-enter.sh
@@ -100,8 +100,9 @@ chroot_add_resolv_conf "$mountPoint" || echo "$0: failed to set up resolv.conf"
     # Run the activation script. Set $LOCALE_ARCHIVE to supress some Perl locale warnings.
     LOCALE_ARCHIVE="$system/sw/lib/locale/locale-archive" IN_NIXOS_ENTER=1 chroot "$mountPoint" "$system/activate" 1>&2 || true
 
-    # Create /tmp
-    chroot "$mountPoint" systemd-tmpfiles --create --remove --exclude-prefix=/dev 1>&2 || true
+    # Create /tmp. This is needed for nix-build and the NixOS activation script to work.
+    # Hide the unhelpful "failed to replace specifiers" errors caused by missing /etc/machine-id.
+    chroot "$mountPoint" "$system/sw/bin/systemd-tmpfiles" --create --remove -E 2> /dev/null || true
 )
 
 unset TMPDIR
diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl
index 212b2b3cd23..c65898b261c 100644
--- a/nixos/modules/installer/tools/nixos-generate-config.pl
+++ b/nixos/modules/installer/tools/nixos-generate-config.pl
@@ -85,12 +85,7 @@ 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;";
+push @attrs, "nixpkgs.hostPlatform = lib.mkDefault \"@system@\";";
 
 
 my $cpuinfo = read_file "/proc/cpuinfo";
@@ -127,9 +122,6 @@ if (-e "/sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors") {
 push @kernelModules, "kvm-intel" if hasCPUFeature "vmx";
 push @kernelModules, "kvm-amd" if hasCPUFeature "svm";
 
-push @attrs, "hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;" if cpuManufacturer "AuthenticAMD";
-push @attrs, "hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;" if cpuManufacturer "GenuineIntel";
-
 
 # Look at the PCI devices and add necessary modules.  Note that most
 # modules are auto-detected so we don't need to list them here.
@@ -203,7 +195,7 @@ sub pciCheck {
     }
 
     # In case this is a virtio scsi device, we need to explicitly make this available.
-    if ($vendor eq "0x1af4" && $device eq "0x1004") {
+    if ($vendor eq "0x1af4" && ($device eq "0x1004" || $device eq "0x1048") ) {
         push @initrdAvailableKernelModules, "virtio_scsi";
     }
 
@@ -324,11 +316,15 @@ if ($virt eq "systemd-nspawn") {
 }
 
 
-# Provide firmware for devices that are not detected by this script,
-# unless we're in a VM/container.
-push @imports, "(modulesPath + \"/installer/scan/not-detected.nix\")"
-    if $virt eq "none";
+# Check if we're on bare metal, not in a VM/container.
+if ($virt eq "none") {
+    # Provide firmware for devices that are not detected by this script.
+    push @imports, "(modulesPath + \"/installer/scan/not-detected.nix\")";
 
+    # Update the microcode.
+    push @attrs, "hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;" if cpuManufacturer "AuthenticAMD";
+    push @attrs, "hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;" if cpuManufacturer "GenuineIntel";
+}
 
 # For a device name like /dev/sda1, find a more stable path like
 # /dev/disk/by-uuid/X or /dev/disk/by-label/Y.
@@ -472,7 +468,7 @@ EOF
     }
 
     # Don't emit tmpfs entry for /tmp, because it most likely comes from the
-    # boot.tmpOnTmpfs option in configuration.nix (managed declaratively).
+    # boot.tmp.useTmpfs option in configuration.nix (managed declaratively).
     next if ($mountPoint eq "/tmp" && $fsType eq "tmpfs");
 
     # Emit the filesystem.
@@ -517,21 +513,6 @@ EOF
     }
 }
 
-# For lack of a better way to determine it, guess whether we should use a
-# bigger font for the console from the display mode on the first
-# framebuffer. A way based on the physical size/actual DPI reported by
-# the monitor would be nice, but I don't know how to do this without X :)
-my $fb_modes_file = "/sys/class/graphics/fb0/modes";
-if (-f $fb_modes_file && -r $fb_modes_file) {
-    my $modes = read_file($fb_modes_file);
-    $modes =~ m/([0-9]+)x([0-9]+)/;
-    my $console_width = $1, my $console_height = $2;
-    if ($console_width > 1920) {
-        push @attrs, "# high-resolution display";
-        push @attrs, 'hardware.video.hidpi.enable = lib.mkDefault true;';
-    }
-}
-
 
 # Generate the hardware configuration file.
 
diff --git a/nixos/modules/installer/tools/nixos-install.sh b/nixos/modules/installer/tools/nixos-install.sh
index e7cf52f5e32..20fec525e70 100644..100755
--- a/nixos/modules/installer/tools/nixos-install.sh
+++ b/nixos/modules/installer/tools/nixos-install.sh
@@ -195,7 +195,20 @@ if [[ -z $noBootLoader ]]; then
     echo "installing the boot loader..."
     # Grub needs an mtab.
     ln -sfn /proc/mounts "$mountPoint"/etc/mtab
-    NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root "$mountPoint" -- /run/current-system/bin/switch-to-configuration boot
+    export mountPoint
+    NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root "$mountPoint" -c "$(cat <<'EOF'
+      # Create a bind mount for each of the mount points inside the target file
+      # system. This preserves the validity of their absolute paths after changing
+      # the root with `nixos-enter`.
+      # Without this the bootloader installation may fail due to options that
+      # contain paths referenced during evaluation, like initrd.secrets.
+      # when not root, re-execute the script in an unshared namespace
+      mount --rbind --mkdir / "$mountPoint"
+      mount --make-rslave "$mountPoint"
+      /run/current-system/bin/switch-to-configuration boot
+      umount -R "$mountPoint" && rmdir "$mountPoint"
+EOF
+)"
 fi
 
 # Ask the user to set a root password, but only if the passwd command
diff --git a/nixos/modules/installer/tools/nixos-version.sh b/nixos/modules/installer/tools/nixos-version.sh
index 59a9c572b41..39e34a3718c 100644
--- a/nixos/modules/installer/tools/nixos-version.sh
+++ b/nixos/modules/installer/tools/nixos-version.sh
@@ -8,11 +8,18 @@ case "$1" in
     ;;
   --hash|--revision)
     if ! [[ @revision@ =~ ^[0-9a-f]+$ ]]; then
-      echo "$0: Nixpkgs commit hash is unknown"
+      echo "$0: Nixpkgs commit hash is unknown" >&2
       exit 1
     fi
     echo "@revision@"
     ;;
+  --configuration-revision)
+    if [[ "@configurationRevision@" =~ "@" ]]; then
+      echo "$0: configuration revision is unknown" >&2
+      exit 1
+    fi
+    echo "@configurationRevision@"
+    ;;
   --json)
     cat <<EOF
 @json@
diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
index e46a2df8fa6..5133ad18f4b 100644
--- a/nixos/modules/installer/tools/tools.nix
+++ b/nixos/modules/installer/tools/tools.nix
@@ -25,6 +25,7 @@ let
     path = makeBinPath [
       pkgs.jq
       nixos-enter
+      pkgs.util-linuxMinimal
     ];
   };
 
@@ -34,7 +35,7 @@ let
     name = "nixos-generate-config";
     src = ./nixos-generate-config.pl;
     perl = "${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl";
-    nixInstantiate = "${pkgs.nix}/bin/nix-instantiate";
+    system = pkgs.stdenv.hostPlatform.system;
     detectvirt = "${config.systemd.package}/bin/systemd-detect-virt";
     btrfs = "${pkgs.btrfs-progs}/bin/btrfs";
     inherit (config.system.nixos-generate-config) configuration desktopConfiguration;
@@ -65,6 +66,9 @@ let
     name = "nixos-enter";
     src = ./nixos-enter.sh;
     inherit (pkgs) runtimeShell;
+    path = makeBinPath [
+      pkgs.util-linuxMinimal
+    ];
   };
 
 in
@@ -123,7 +127,7 @@ in
     system.nixos-generate-config.configuration = mkDefault ''
       # Edit this configuration file to define what should be installed on
       # your system.  Help is available in the configuration.nix(5) man page
-      # and in the NixOS manual (accessible by running ‘nixos-help’).
+      # and in the NixOS manual (accessible by running `nixos-help`).
 
       { config, pkgs, ... }:
 
@@ -159,10 +163,7 @@ in
       $desktopConfiguration
         # Configure keymap in X11
         # services.xserver.layout = "us";
-        # services.xserver.xkbOptions = {
-        #   "eurosign:e";
-        #   "caps:escape" # map caps to escape.
-        # };
+        # services.xserver.xkbOptions = "eurosign:e,caps:escape";
 
         # Enable CUPS to print documents.
         # services.printing.enable = true;
@@ -180,7 +181,7 @@ in
         #   extraGroups = [ "wheel" ]; # Enable ‘sudo’ for the user.
         #   packages = with pkgs; [
         #     firefox
-        #     thunderbird
+        #     tree
         #   ];
         # };
 
@@ -217,7 +218,7 @@ in
 
         # 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
+        # on your system were taken. It's perfectly fine and recommended to leave
         # this value at the release version of the first install of this system.
         # Before changing this value read the documentation for this option
         # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
@@ -235,6 +236,8 @@ in
         nixos-enter
       ] ++ lib.optional (nixos-option != null) nixos-option;
 
+    documentation.man.man-db.skipPackages = [ nixos-version ];
+
     system.build = {
       inherit nixos-install nixos-generate-config nixos-option nixos-rebuild nixos-enter;
     };
diff --git a/nixos/modules/misc/documentation.nix b/nixos/modules/misc/documentation.nix
index 64a8f7846b4..31486a2216a 100644
--- a/nixos/modules/misc/documentation.nix
+++ b/nixos/modules/misc/documentation.nix
@@ -38,6 +38,7 @@ let
           modules = [ {
             _module.check = false;
           } ] ++ docModules.eager;
+          class = "nixos";
           specialArgs = specialArgs // {
             pkgs = scrubDerivations "pkgs" pkgs;
             # allow access to arbitrary options for eager modules, eg for getting
@@ -50,7 +51,7 @@ let
           (name: value:
             let
               wholeName = "${namePrefix}.${name}";
-              guard = lib.warn "Attempt to evaluate package ${wholeName} in option documentation; this is not supported and will eventually be an error. Use `mkPackageOption` or `literalExpression` instead.";
+              guard = lib.warn "Attempt to evaluate package ${wholeName} in option documentation; this is not supported and will eventually be an error. Use `mkPackageOption{,MD}` or `literalExpression` instead.";
             in if isAttrs value then
               scrubDerivations wholeName value
               // optionalAttrs (isDerivation value) {
@@ -131,7 +132,8 @@ let
     desktopItem = pkgs.makeDesktopItem {
       name = "nixos-manual";
       desktopName = "NixOS Manual";
-      genericName = "View NixOS documentation in a web browser";
+      genericName = "System Manual";
+      comment = "View NixOS documentation in a web browser";
       icon = "nix-snowflake";
       exec = "nixos-help";
       categories = ["System"];
@@ -357,6 +359,14 @@ in
     (mkIf cfg.nixos.enable {
       system.build.manual = manual;
 
+      system.activationScripts.check-manual-docbook = ''
+        if [[ $(cat ${manual.optionsUsedDocbook}) = 1 ]]; then
+          echo -e "\e[31;1mwarning\e[0m: This configuration contains option documentation in docbook." \
+                  "Support for docbook is deprecated and will be removed after NixOS 23.05." \
+                  "See nix-store --read-log ${builtins.unsafeDiscardStringContext manual.optionsJSON.drvPath}"
+        fi
+      '';
+
       environment.systemPackages = []
         ++ optional cfg.man.enable manual.manpages
         ++ optionals cfg.doc.enable [ manual.manualHTML nixos-help ];
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index 17ea04cb4ec..5b278b5e806 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -233,7 +233,7 @@ in
       # nix-serve = 199; # unused, removed 2020-12-12
       #tvheadend = 200; # dynamically allocated as of 2021-09-18
       uwsgi = 201;
-      gitit = 202;
+      # gitit = 202; # unused, module was removed 2023-04-03
       riemanntools = 203;
       subsonic = 204;
       # riak = 205; # unused, remove 2022-07-22
@@ -338,7 +338,7 @@ in
       lidarr = 306;
       slurm = 307;
       kapacitor = 308;
-      solr = 309;
+      # solr = 309; removed 2023-03-16
       alerta = 310;
       minetest = 311;
       rss2email = 312;
@@ -648,7 +648,7 @@ in
       lidarr = 306;
       slurm = 307;
       kapacitor = 308;
-      solr = 309;
+      # solr = 309; removed 2023-03-16
       alerta = 310;
       minetest = 311;
       rss2email = 312;
diff --git a/nixos/modules/misc/man-db.nix b/nixos/modules/misc/man-db.nix
index 299b11d1fce..2b980561218 100644
--- a/nixos/modules/misc/man-db.nix
+++ b/nixos/modules/misc/man-db.nix
@@ -13,11 +13,21 @@ in
         example = false;
       };
 
+      skipPackages = lib.mkOption {
+        type = lib.types.listOf lib.types.package;
+        default = [];
+        internal = true;
+        description = lib.mdDoc ''
+          Packages to *not* include in the man-db.
+          This can be useful to avoid unnecessary rebuilds due to packages that change frequently, like nixos-version.
+        '';
+      };
+
       manualPages = lib.mkOption {
         type = lib.types.path;
         default = pkgs.buildEnv {
           name = "man-paths";
-          paths = config.environment.systemPackages;
+          paths = lib.subtractLists cfg.skipPackages config.environment.systemPackages;
           pathsToLink = [ "/share/man" ];
           extraOutputsToInstall = [ "man" ]
             ++ lib.optionals config.documentation.dev.enable [ "devman" ];
diff --git a/nixos/modules/misc/meta.nix b/nixos/modules/misc/meta.nix
index e1d16f802ce..95f2765aff1 100644
--- a/nixos/modules/misc/meta.nix
+++ b/nixos/modules/misc/meta.nix
@@ -47,7 +47,7 @@ in
       doc = mkOption {
         type = docFile;
         internal = true;
-        example = "./meta.chapter.xml";
+        example = "./meta.chapter.md";
         description = lib.mdDoc ''
           Documentation prologue for the set of options of each module.  This
           option should be defined at most once per module.
diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix
index b3cdaf5568d..447f8193855 100644
--- a/nixos/modules/misc/version.nix
+++ b/nixos/modules/misc/version.nix
@@ -16,18 +16,20 @@ let
     ) + "\n";
 
   osReleaseContents = {
-    NAME = "NixOS";
-    ID = "nixos";
+    NAME = "${cfg.distroName}";
+    ID = "${cfg.distroId}";
     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})";
+    PRETTY_NAME = "${cfg.distroName} ${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";
+    HOME_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/";
+    DOCUMENTATION_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/learn.html";
+    SUPPORT_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/community.html";
+    BUG_REPORT_URL = lib.optionalString (cfg.distroId == "nixos") "https://github.com/NixOS/nixpkgs/issues";
+  } // lib.optionalAttrs (cfg.variant_id != null) {
+    VARIANT_ID = cfg.variant_id;
   };
 
   initrdReleaseContents = osReleaseContents // {
@@ -87,8 +89,35 @@ in
       description = lib.mdDoc "The NixOS release code name (e.g. `Emu`).";
     };
 
+    nixos.distroId = mkOption {
+      internal = true;
+      type = types.str;
+      default = "nixos";
+      description = lib.mdDoc "The id of the operating system";
+    };
+
+    nixos.distroName = mkOption {
+      internal = true;
+      type = types.str;
+      default = "NixOS";
+      description = lib.mdDoc "The name of the operating system";
+    };
+
+    nixos.variant_id = mkOption {
+      type = types.nullOr (types.strMatching "^[a-z0-9._-]+$");
+      default = null;
+      description = lib.mdDoc "A lower-case string identifying a specific variant or edition of the operating system";
+      example = "installer";
+    };
+
     stateVersion = mkOption {
       type = types.str;
+      # 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
+      apply = v:
+        lib.warnIf (options.system.stateVersion.highestPrio == (lib.mkOptionDefault { }).priority)
+          "system.stateVersion is not set, defaulting to ${v}. Read why this matters on https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersion."
+          v;
       default = cfg.release;
       defaultText = literalExpression "config.${opt.release}";
       description = lib.mdDoc ''
@@ -101,7 +130,7 @@ in
         to be compatible. The effect is that NixOS will use
         defaults corresponding to the specified release (such as using
         an older version of PostgreSQL).
-        It‘s perfectly fine and recommended to leave this value at the
+        It’s perfectly fine and recommended to leave this value at the
         release version of the first install of this system.
         Changing this option will not upgrade your system. In fact it
         is meant to stay constant exactly when you upgrade your system.
@@ -140,23 +169,15 @@ in
     environment.etc = {
       "lsb-release".text = attrsToText {
         LSB_VERSION = "${cfg.release} (${cfg.codeName})";
-        DISTRIB_ID = "nixos";
+        DISTRIB_ID = "${cfg.distroId}";
         DISTRIB_RELEASE = cfg.release;
         DISTRIB_CODENAME = toLower cfg.codeName;
-        DISTRIB_DESCRIPTION = "NixOS ${cfg.release} (${cfg.codeName})";
+        DISTRIB_DESCRIPTION = "${cfg.distroName} ${cfg.release} (${cfg.codeName})";
       };
 
       "os-release".text = attrsToText osReleaseContents;
     };
 
-    # 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/module-list.nix b/nixos/modules/module-list.nix
index 6ec6c74565c..4941b78a577 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -20,9 +20,10 @@
   ./config/nsswitch.nix
   ./config/power-management.nix
   ./config/pulseaudio.nix
-  ./config/qt5.nix
+  ./config/qt.nix
   ./config/resolvconf.nix
   ./config/shells-environment.nix
+  ./config/stevenblack.nix
   ./config/swap.nix
   ./config/sysctl.nix
   ./config/system-environment.nix
@@ -52,11 +53,13 @@
   ./hardware/cpu/intel-sgx.nix
   ./hardware/device-tree.nix
   ./hardware/digitalbitbox.nix
+  ./hardware/flipperzero.nix
   ./hardware/flirc.nix
   ./hardware/gkraken.nix
   ./hardware/gpgsmartcards.nix
   ./hardware/hackrf.nix
   ./hardware/i2c.nix
+  ./hardware/keyboard/qmk.nix
   ./hardware/keyboard/teck.nix
   ./hardware/keyboard/uhk.nix
   ./hardware/keyboard/zsa.nix
@@ -92,16 +95,15 @@
   ./hardware/video/bumblebee.nix
   ./hardware/video/capture/mwprocapture.nix
   ./hardware/video/displaylink.nix
-  ./hardware/video/hidpi.nix
   ./hardware/video/nvidia.nix
   ./hardware/video/switcheroo-control.nix
   ./hardware/video/uvcvideo/default.nix
   ./hardware/video/webcam/facetimehd.nix
+  ./hardware/video/webcam/ipu6.nix
   ./hardware/wooting.nix
   ./hardware/xone.nix
   ./hardware/xpadneo.nix
   ./i18n/input-method/default.nix
-  ./i18n/input-method/fcitx.nix
   ./i18n/input-method/fcitx5.nix
   ./i18n/input-method/hime.nix
   ./i18n/input-method/ibus.nix
@@ -147,9 +149,11 @@
   ./programs/cdemu.nix
   ./programs/cfs-zen-tweaks.nix
   ./programs/chromium.nix
+  ./programs/clash-verge.nix
   ./programs/cnping.nix
   ./programs/command-not-found/command-not-found.nix
   ./programs/criu.nix
+  ./programs/darling.nix
   ./programs/dconf.nix
   ./programs/digitalbitbox/default.nix
   ./programs/dmrconfig.nix
@@ -168,10 +172,10 @@
   ./programs/fuse.nix
   ./programs/fzf.nix
   ./programs/gamemode.nix
+  ./programs/gamescope.nix
   ./programs/geary.nix
   ./programs/git.nix
   ./programs/gnome-disks.nix
-  ./programs/gnome-documents.nix
   ./programs/gnome-terminal.nix
   ./programs/gnupg.nix
   ./programs/gpaste.nix
@@ -179,7 +183,10 @@
   ./programs/haguichi.nix
   ./programs/hamster.nix
   ./programs/htop.nix
+  ./programs/hyprland.nix
+  ./programs/iay.nix
   ./programs/iftop.nix
+  ./programs/i3lock.nix
   ./programs/iotop.nix
   ./programs/java.nix
   ./programs/k3b.nix
@@ -193,6 +200,8 @@
   ./programs/mdevctl.nix
   ./programs/mepo.nix
   ./programs/mininet.nix
+  ./programs/minipro.nix
+  ./programs/miriway.nix
   ./programs/mosh.nix
   ./programs/msmtp.nix
   ./programs/mtr.nix
@@ -200,6 +209,8 @@
   ./programs/nbd.nix
   ./programs/neovim.nix
   ./programs/nethoscope.nix
+  ./programs/nexttrace.nix
+  ./programs/nix-index.nix
   ./programs/nix-ld.nix
   ./programs/nm-applet.nix
   ./programs/nncp.nix
@@ -211,13 +222,16 @@
   ./programs/partition-manager.nix
   ./programs/plotinus.nix
   ./programs/proxychains.nix
+  ./programs/qdmr.nix
   ./programs/qt5ct.nix
+  ./programs/regreet.nix
   ./programs/rog-control-center.nix
   ./programs/rust-motd.nix
   ./programs/screen.nix
   ./programs/seahorse.nix
   ./programs/sedutil.nix
   ./programs/shadow.nix
+  ./programs/sharing.nix
   ./programs/singularity.nix
   ./programs/skim.nix
   ./programs/slock.nix
@@ -243,6 +257,7 @@
   ./programs/waybar.nix
   ./programs/weylus.nix
   ./programs/wireshark.nix
+  ./programs/xastir.nix
   ./programs/wshowkeys.nix
   ./programs/xfconf.nix
   ./programs/xfs_quota.nix
@@ -267,6 +282,7 @@
   ./security/doas.nix
   ./security/duosec.nix
   ./security/google_oslogin.nix
+  ./security/ipa.nix
   ./security/lock-kernel-modules.nix
   ./security/misc.nix
   ./security/oath.nix
@@ -290,6 +306,8 @@
   ./services/amqp/rabbitmq.nix
   ./services/audio/alsa.nix
   ./services/audio/botamusique.nix
+  ./services/audio/gmediarender.nix
+  ./services/audio/gonic.nix
   ./services/audio/hqplayerd.nix
   ./services/audio/icecast.nix
   ./services/audio/jack.nix
@@ -306,6 +324,7 @@
   ./services/audio/snapserver.nix
   ./services/audio/spotifyd.nix
   ./services/audio/squeezelite.nix
+  ./services/audio/tts.nix
   ./services/audio/ympd.nix
   ./services/backup/automysqlbackup.nix
   ./services/backup/bacula.nix
@@ -365,6 +384,8 @@
   ./services/continuous-integration/jenkins/default.nix
   ./services/continuous-integration/jenkins/job-builder.nix
   ./services/continuous-integration/jenkins/slave.nix
+  ./services/continuous-integration/woodpecker/agents.nix
+  ./services/continuous-integration/woodpecker/server.nix
   ./services/databases/aerospike.nix
   ./services/databases/cassandra.nix
   ./services/databases/clickhouse.nix
@@ -377,6 +398,7 @@
   ./services/databases/hbase-standalone.nix
   ./services/databases/influxdb.nix
   ./services/databases/influxdb2.nix
+  ./services/databases/lldap.nix
   ./services/databases/memcached.nix
   ./services/databases/monetdb.nix
   ./services/databases/mongodb.nix
@@ -393,6 +415,9 @@
   ./services/desktops/bamf.nix
   ./services/desktops/blueman.nix
   ./services/desktops/cpupower-gui.nix
+  ./services/desktops/deepin/dde-api.nix
+  ./services/desktops/deepin/app-services.nix
+  ./services/desktops/deepin/dde-daemon.nix
   ./services/desktops/dleyna-renderer.nix
   ./services/desktops/dleyna-server.nix
   ./services/desktops/espanso.nix
@@ -417,17 +442,18 @@
   ./services/desktops/gvfs.nix
   ./services/desktops/malcontent.nix
   ./services/desktops/neard.nix
-  ./services/desktops/pipewire/pipewire-media-session.nix
   ./services/desktops/pipewire/pipewire.nix
   ./services/desktops/pipewire/wireplumber.nix
   ./services/desktops/profile-sync-daemon.nix
   ./services/desktops/system-config-printer.nix
+  ./services/desktops/system76-scheduler.nix
   ./services/desktops/telepathy.nix
   ./services/desktops/tumbler.nix
   ./services/desktops/zeitgeist.nix
   ./services/development/blackfire.nix
   ./services/development/bloop.nix
   ./services/development/distccd.nix
+  ./services/development/gemstash.nix
   ./services/development/hoogle.nix
   ./services/development/jupyter/default.nix
   ./services/development/jupyterhub/default.nix
@@ -496,6 +522,8 @@
   ./services/hardware/usbmuxd.nix
   ./services/hardware/usbrelayd.nix
   ./services/hardware/vdr.nix
+  ./services/hardware/keyd.nix
+  ./services/home-automation/esphome.nix
   ./services/home-automation/evcc.nix
   ./services/home-automation/home-assistant.nix
   ./services/home-automation/zigbee2mqtt.nix
@@ -517,12 +545,14 @@
   ./services/logging/syslog-ng.nix
   ./services/logging/syslogd.nix
   ./services/logging/vector.nix
+  ./services/logging/ulogd.nix
   ./services/mail/clamsmtp.nix
   ./services/mail/davmail.nix
   ./services/mail/dkimproxy-out.nix
   ./services/mail/dovecot.nix
   ./services/mail/dspam.nix
   ./services/mail/exim.nix
+  ./services/mail/goeland.nix
   ./services/mail/listmonk.nix
   ./services/mail/maddy.nix
   ./services/mail/mail.nix
@@ -546,6 +576,7 @@
   ./services/mail/schleuder.nix
   ./services/mail/spamassassin.nix
   ./services/mail/sympa.nix
+  ./services/mail/zeyple.nix
   ./services/matrix/appservice-discord.nix
   ./services/matrix/appservice-irc.nix
   ./services/matrix/conduit.nix
@@ -553,6 +584,7 @@
   ./services/matrix/mautrix-facebook.nix
   ./services/matrix/mautrix-telegram.nix
   ./services/matrix/mjolnir.nix
+  ./services/matrix/mx-puppet-discord.nix
   ./services/matrix/pantalaimon.nix
   ./services/matrix/synapse.nix
   ./services/misc/airsonic.nix
@@ -562,6 +594,7 @@
   ./services/misc/atuin.nix
   ./services/misc/autofs.nix
   ./services/misc/autorandr.nix
+  ./services/misc/autosuspend.nix
   ./services/misc/bazarr.nix
   ./services/misc/beanstalkd.nix
   ./services/misc/bees.nix
@@ -593,7 +626,6 @@
   ./services/misc/gammu-smsd.nix
   ./services/misc/geoipupdate.nix
   ./services/misc/gitea.nix
-  # ./services/misc/gitit.nix
   ./services/misc/gitlab.nix
   ./services/misc/gitolite.nix
   ./services/misc/gitweb.nix
@@ -608,6 +640,7 @@
   ./services/misc/irkerd.nix
   ./services/misc/jackett.nix
   ./services/misc/jellyfin.nix
+  ./services/misc/jellyseerr.nix
   ./services/misc/klipper.nix
   ./services/misc/languagetool.nix
   ./services/misc/leaps.nix
@@ -620,7 +653,6 @@
   ./services/misc/mediatomb.nix
   ./services/misc/metabase.nix
   ./services/misc/moonraker.nix
-  ./services/misc/mx-puppet-discord.nix
   ./services/misc/n8n.nix
   ./services/misc/nitter.nix
   ./services/misc/nix-daemon.nix
@@ -646,8 +678,10 @@
   ./services/misc/polaris.nix
   ./services/misc/portunus.nix
   ./services/misc/prowlarr.nix
+  ./services/misc/pufferpanel.nix
   ./services/misc/pykms.nix
   ./services/misc/radarr.nix
+  ./services/misc/readarr.nix
   ./services/misc/redmine.nix
   ./services/misc/ripple-data-api.nix
   ./services/misc/rippled.nix
@@ -687,6 +721,7 @@
   ./services/monitoring/arbtt.nix
   ./services/monitoring/bosun.nix
   ./services/monitoring/cadvisor.nix
+  ./services/monitoring/cockpit.nix
   ./services/monitoring/collectd.nix
   ./services/monitoring/das_watchdog.nix
   ./services/monitoring/datadog-agent.nix
@@ -713,6 +748,7 @@
   ./services/monitoring/nagios.nix
   ./services/monitoring/netdata.nix
   ./services/monitoring/parsedmarc.nix
+  ./services/monitoring/prometheus/alertmanager-irc-relay.nix
   ./services/monitoring/prometheus/alertmanager.nix
   ./services/monitoring/prometheus/default.nix
   ./services/monitoring/prometheus/exporters.nix
@@ -724,6 +760,7 @@
   ./services/monitoring/riemann.nix
   ./services/monitoring/scollector.nix
   ./services/monitoring/smartd.nix
+  ./services/monitoring/statsd.nix
   ./services/monitoring/sysstat.nix
   ./services/monitoring/teamviewer.nix
   ./services/monitoring/telegraf.nix
@@ -784,6 +821,7 @@
   ./services/networking/bitlbee.nix
   ./services/networking/blockbook-frontend.nix
   ./services/networking/blocky.nix
+  ./services/networking/cgit.nix
   ./services/networking/charybdis.nix
   ./services/networking/chisel-server.nix
   ./services/networking/cjdns.nix
@@ -819,6 +857,8 @@
   ./services/networking/firefox-syncserver.nix
   ./services/networking/fireqos.nix
   ./services/networking/firewall.nix
+  ./services/networking/firewall-iptables.nix
+  ./services/networking/firewall-nftables.nix
   ./services/networking/flannel.nix
   ./services/networking/freenet.nix
   ./services/networking/freeradius.nix
@@ -845,6 +885,7 @@
   ./services/networking/i2pd.nix
   ./services/networking/icecream/daemon.nix
   ./services/networking/icecream/scheduler.nix
+  ./services/networking/imaginary.nix
   ./services/networking/inspircd.nix
   ./services/networking/iodine.nix
   ./services/networking/iperf3.nix
@@ -852,6 +893,7 @@
   ./services/networking/iscsi/initiator.nix
   ./services/networking/iscsi/root-initiator.nix
   ./services/networking/iscsi/target.nix
+  ./services/networking/ivpn.nix
   ./services/networking/iwd.nix
   ./services/networking/jibri/default.nix
   ./services/networking/jicofo.nix
@@ -874,7 +916,6 @@
   ./services/networking/miredo.nix
   ./services/networking/mjpg-streamer.nix
   ./services/networking/mmsd.nix
-  ./services/networking/mosquitto.nix
   ./services/networking/monero.nix
   ./services/networking/morty.nix
   ./services/networking/mosquitto.nix
@@ -889,12 +930,15 @@
   ./services/networking/namecoind.nix
   ./services/networking/nar-serve.nix
   ./services/networking/nat.nix
+  ./services/networking/nat-iptables.nix
+  ./services/networking/nat-nftables.nix
   ./services/networking/nats.nix
   ./services/networking/nbd.nix
   ./services/networking/ncdns.nix
   ./services/networking/ndppd.nix
   ./services/networking/nebula.nix
   ./services/networking/netbird.nix
+  ./services/networking/networkd-dispatcher.nix
   ./services/networking/networkmanager.nix
   ./services/networking/nextdns.nix
   ./services/networking/nftables.nix
@@ -922,6 +966,8 @@
   ./services/networking/owamp.nix
   ./services/networking/pdns-recursor.nix
   ./services/networking/pdnsd.nix
+  ./services/networking/peroxide.nix
+  ./services/networking/picosnitch.nix
   ./services/networking/pixiecore.nix
   ./services/networking/pleroma.nix
   ./services/networking/polipo.nix
@@ -1005,9 +1051,12 @@
   ./services/networking/wasabibackend.nix
   ./services/networking/websockify.nix
   ./services/networking/wg-netmanager.nix
+  ./services/networking/webhook.nix
   ./services/networking/wg-quick.nix
+  ./services/networking/wgautomesh.nix
   ./services/networking/wireguard.nix
   ./services/networking/wpa_supplicant.nix
+  ./services/networking/wstunnel.nix
   ./services/networking/x2goserver.nix
   ./services/networking/xandikos.nix
   ./services/networking/xinetd.nix
@@ -1021,6 +1070,7 @@
   ./services/networking/znc/default.nix
   ./services/printing/cupsd.nix
   ./services/printing/ipp-usb.nix
+  ./services/printing/cups-pdf.nix
   ./services/scheduling/atd.nix
   ./services/scheduling/cron.nix
   ./services/scheduling/fcron.nix
@@ -1029,8 +1079,10 @@
   ./services/search/hound.nix
   ./services/search/kibana.nix
   ./services/search/meilisearch.nix
-  ./services/search/solr.nix
+  ./services/search/opensearch.nix
+  ./services/search/qdrant.nix
   ./services/security/aesmd.nix
+  ./services/security/authelia.nix
   ./services/security/certmgr.nix
   ./services/security/cfssl.nix
   ./services/security/clamav.nix
@@ -1063,6 +1115,7 @@
   ./services/security/torsocks.nix
   ./services/security/usbguard.nix
   ./services/security/vault.nix
+  ./services/security/vault-agent.nix
   ./services/security/vaultwarden/default.nix
   ./services/security/yubikey-agent.nix
   ./services/system/automatic-timezoned.nix
@@ -1092,17 +1145,21 @@
   ./services/video/epgstation/default.nix
   ./services/video/mirakurun.nix
   ./services/video/replay-sorcery.nix
-  ./services/video/rtsp-simple-server.nix
+  ./services/video/mediamtx.nix
   ./services/video/unifi-video.nix
+  ./services/video/v4l2-relayd.nix
   ./services/wayland/cage.nix
+  ./services/web-apps/akkoma.nix
   ./services/web-apps/alps.nix
   ./services/web-apps/atlassian/confluence.nix
   ./services/web-apps/atlassian/crowd.nix
   ./services/web-apps/atlassian/jira.nix
-  ./services/web-apps/baget.nix
   ./services/web-apps/bookstack.nix
   ./services/web-apps/calibre-web.nix
+  ./services/web-apps/coder.nix
   ./services/web-apps/changedetection-io.nix
+  ./services/web-apps/chatgpt-retrieval-plugin.nix
+  ./services/web-apps/cloudlog.nix
   ./services/web-apps/code-server.nix
   ./services/web-apps/convos.nix
   ./services/web-apps/dex.nix
@@ -1123,24 +1180,28 @@
   ./services/web-apps/hledger-web.nix
   ./services/web-apps/icingaweb2/icingaweb2.nix
   ./services/web-apps/icingaweb2/module-monitoring.nix
-  ./services/web-apps/ihatemoney
   ./services/web-apps/invidious.nix
   ./services/web-apps/invoiceplane.nix
   ./services/web-apps/isso.nix
   ./services/web-apps/jirafeau.nix
   ./services/web-apps/jitsi-meet.nix
+  ./services/web-apps/kasmweb/default.nix
+  ./services/web-apps/kavita.nix
   ./services/web-apps/keycloak.nix
   ./services/web-apps/komga.nix
   ./services/web-apps/lemmy.nix
   ./services/web-apps/limesurvey.nix
+  ./services/web-apps/mainsail.nix
   ./services/web-apps/mastodon.nix
   ./services/web-apps/matomo.nix
   ./services/web-apps/mattermost.nix
   ./services/web-apps/mediawiki.nix
   ./services/web-apps/miniflux.nix
+  ./services/web-apps/monica.nix
   ./services/web-apps/moodle.nix
   ./services/web-apps/netbox.nix
   ./services/web-apps/nextcloud.nix
+  ./services/web-apps/nextcloud-notify_push.nix
   ./services/web-apps/nexus.nix
   ./services/web-apps/nifi.nix
   ./services/web-apps/node-red.nix
@@ -1151,6 +1212,7 @@
   ./services/web-apps/peertube.nix
   ./services/web-apps/pgpkeyserver-lite.nix
   ./services/web-apps/phylactery.nix
+  ./services/web-apps/photoprism.nix
   ./services/web-apps/pict-rs.nix
   ./services/web-apps/plantuml-server.nix
   ./services/web-apps/plausible.nix
@@ -1193,6 +1255,7 @@
   ./services/web-servers/nginx/gitweb.nix
   ./services/web-servers/phpfpm/default.nix
   ./services/web-servers/pomerium.nix
+  ./services/web-servers/stargazer.nix
   ./services/web-servers/tomcat.nix
   ./services/web-servers/traefik.nix
   ./services/web-servers/trafficserver/default.nix
@@ -1237,6 +1300,7 @@
   ./services/x11/window-managers/bspwm.nix
   ./services/x11/window-managers/katriawm.nix
   ./services/x11/window-managers/metacity.nix
+  ./services/x11/window-managers/nimdow.nix
   ./services/x11/window-managers/none.nix
   ./services/x11/window-managers/twm.nix
   ./services/x11/window-managers/windowlab.nix
@@ -1285,9 +1349,12 @@
   ./system/boot/systemd/logind.nix
   ./system/boot/systemd/nspawn.nix
   ./system/boot/systemd/oomd.nix
+  ./system/boot/systemd/repart.nix
   ./system/boot/systemd/shutdown.nix
   ./system/boot/systemd/tmpfiles.nix
   ./system/boot/systemd/user.nix
+  ./system/boot/systemd/userdbd.nix
+  ./system/boot/systemd/homed.nix
   ./system/boot/timesyncd.nix
   ./system/boot/tmp.nix
   ./system/boot/uvesafb.nix
@@ -1302,6 +1369,7 @@
   ./tasks/filesystems/btrfs.nix
   ./tasks/filesystems/cifs.nix
   ./tasks/filesystems/ecryptfs.nix
+  ./tasks/filesystems/envfs.nix
   ./tasks/filesystems/exfat.nix
   ./tasks/filesystems/ext.nix
   ./tasks/filesystems/f2fs.nix
@@ -1342,6 +1410,7 @@
   ./virtualisation/lxc.nix
   ./virtualisation/lxcfs.nix
   ./virtualisation/lxd.nix
+  ./virtualisation/multipass.nix
   ./virtualisation/nixos-containers.nix
   ./virtualisation/oci-containers.nix
   ./virtualisation/openstack-options.nix
diff --git a/nixos/modules/profiles/base.nix b/nixos/modules/profiles/base.nix
index 616b2470dcb..9f32f85a61e 100644
--- a/nixos/modules/profiles/base.nix
+++ b/nixos/modules/profiles/base.nix
@@ -1,5 +1,5 @@
 # This module defines the software packages included in the "minimal"
-# installation CD.  It might be useful elsewhere.
+# installation CD. It might be useful elsewhere.
 
 { config, lib, pkgs, ... }:
 
@@ -17,7 +17,6 @@
     pkgs.ddrescue
     pkgs.ccrypt
     pkgs.cryptsetup # needed for dm-crypt volumes
-    pkgs.mkpasswd # for generating password files
 
     # Some text editors.
     (pkgs.vim.customize {
@@ -32,9 +31,9 @@
     pkgs.fuse
     pkgs.fuse3
     pkgs.sshfs-fuse
-    pkgs.rsync
     pkgs.socat
     pkgs.screen
+    pkgs.tcpdump
 
     # Hardware-related tools.
     pkgs.sdparm
@@ -44,22 +43,14 @@
     pkgs.usbutils
     pkgs.nvme-cli
 
-    # Tools to create / manipulate filesystems.
-    pkgs.ntfsprogs # for resizing NTFS partitions
-    pkgs.dosfstools
-    pkgs.mtools
-    pkgs.xfsprogs.bin
-    pkgs.jfsutils
-    pkgs.f2fs-tools
-
     # Some compression/archiver tools.
     pkgs.unzip
     pkgs.zip
   ];
 
-  # Include support for various filesystems.
+  # Include support for various filesystems and tools to create / manipulate them.
   boot.supportedFilesystems =
-    [ "btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs" ] ++
+    [ "btrfs" "cifs" "f2fs" "jfs" "ntfs" "reiserfs" "vfat" "xfs" ] ++
     lib.optional (lib.meta.availableOn pkgs.stdenv.hostPlatform config.boot.zfs.package) "zfs";
 
   # Configure host id for ZFS to work
diff --git a/nixos/modules/profiles/installation-device.nix b/nixos/modules/profiles/installation-device.nix
index ae9be08c8d8..32884f4b875 100644
--- a/nixos/modules/profiles/installation-device.nix
+++ b/nixos/modules/profiles/installation-device.nix
@@ -20,6 +20,7 @@ with lib;
     ];
 
   config = {
+    system.nixos.variant_id = lib.mkDefault "installer";
 
     # Enable in installer, even if the minimal profile disables it.
     documentation.enable = mkImageMediaOverride true;
@@ -51,9 +52,9 @@ with lib;
     services.getty.helpLine = ''
       The "nixos" and "root" accounts have empty passwords.
 
-      An ssh daemon is running. You then must set a password
-      for either "root" or "nixos" with `passwd` or add an ssh key
-      to /home/nixos/.ssh/authorized_keys be able to login.
+      To log in over ssh you must set a password for either "nixos" or "root"
+      with `passwd` (prefix with `sudo` for "root"), or add your public key to
+      /home/nixos/.ssh/authorized_keys or /root/.ssh/authorized_keys.
 
       If you need a wireless connection, type
       `sudo systemctl start wpa_supplicant` and configure a
@@ -64,14 +65,14 @@ with lib;
       start the graphical user interface.
     '';
 
-    # We run sshd by default. Login via root is only possible after adding a
-    # password via "passwd" or by adding a ssh key to /home/nixos/.ssh/authorized_keys.
+    # We run sshd by default. Login is only possible after adding a
+    # password via "passwd" or by adding a ssh key to ~/.ssh/authorized_keys.
     # The latter one is particular useful if keys are manually added to
     # installation device for head-less systems i.e. arm boards by manually
     # mounting the storage in a different system.
     services.openssh = {
       enable = true;
-      permitRootLogin = "yes";
+      settings.PermitRootLogin = "yes";
     };
 
     # Enable wpa_supplicant, but don't start it by default.
diff --git a/nixos/modules/profiles/keys/ssh_host_ed25519_key b/nixos/modules/profiles/keys/ssh_host_ed25519_key
new file mode 100644
index 00000000000..b1848979536
--- /dev/null
+++ b/nixos/modules/profiles/keys/ssh_host_ed25519_key
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACCQVnMW/wZWqrdWrjrRPhfEFFq1KLYguagSflLhFnVQmwAAAJASuMMnErjD
+JwAAAAtzc2gtZWQyNTUxOQAAACCQVnMW/wZWqrdWrjrRPhfEFFq1KLYguagSflLhFnVQmw
+AAAEDIN2VWFyggtoSPXcAFy8dtG1uAig8sCuyE21eMDt2GgJBWcxb/Blaqt1auOtE+F8QU
+WrUotiC5qBJ+UuEWdVCbAAAACnJvb3RAbml4b3MBAgM=
+-----END OPENSSH PRIVATE KEY-----
diff --git a/nixos/modules/profiles/keys/ssh_host_ed25519_key.pub b/nixos/modules/profiles/keys/ssh_host_ed25519_key.pub
new file mode 100644
index 00000000000..2c45826715f
--- /dev/null
+++ b/nixos/modules/profiles/keys/ssh_host_ed25519_key.pub
@@ -0,0 +1 @@
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJBWcxb/Blaqt1auOtE+F8QUWrUotiC5qBJ+UuEWdVCb root@nixos
diff --git a/nixos/modules/profiles/macos-builder.nix b/nixos/modules/profiles/macos-builder.nix
new file mode 100644
index 00000000000..768c673e7f3
--- /dev/null
+++ b/nixos/modules/profiles/macos-builder.nix
@@ -0,0 +1,239 @@
+{ config, lib, pkgs, ... }:
+
+let
+  keysDirectory = "/var/keys";
+
+  user = "builder";
+
+  keyType = "ed25519";
+
+  cfg = config.virtualisation.darwin-builder;
+
+in
+
+{
+  imports = [
+    ../virtualisation/qemu-vm.nix
+
+    # Avoid a dependency on stateVersion
+    {
+      disabledModules = [
+        ../virtualisation/nixos-containers.nix
+        ../services/x11/desktop-managers/xterm.nix
+      ];
+      config = { };
+      options.boot.isContainer = lib.mkOption { default = false; internal = true; };
+    }
+  ];
+
+  options.virtualisation.darwin-builder = with lib; {
+    diskSize = mkOption {
+      default = 20 * 1024;
+      type = types.int;
+      example = 30720;
+      description = "The maximum disk space allocated to the runner in MB";
+    };
+    memorySize = mkOption {
+      default = 3 * 1024;
+      type = types.int;
+      example = 8192;
+      description = "The runner's memory in MB";
+    };
+    min-free = mkOption {
+      default = 1024 * 1024 * 1024;
+      type = types.int;
+      example = 1073741824;
+      description = ''
+        The threshold (in bytes) of free disk space left at which to
+        start garbage collection on the runner
+      '';
+    };
+    max-free = mkOption {
+      default = 3 * 1024 * 1024 * 1024;
+      type = types.int;
+      example = 3221225472;
+      description = ''
+        The threshold (in bytes) of free disk space left at which to
+        stop garbage collection on the runner
+      '';
+    };
+    workingDirectory = mkOption {
+       default = ".";
+       type = types.str;
+       example = "/var/lib/darwin-builder";
+       description = ''
+         The working directory to use to run the script. When running
+         as part of a flake will need to be set to a non read-only filesystem.
+       '';
+    };
+    hostPort = mkOption {
+      default = 22;
+      type = types.int;
+      example = 31022;
+      description = ''
+        The localhost host port to forward TCP to the guest port.
+      '';
+    };
+  };
+
+  config = {
+    # The builder is not intended to be used interactively
+    documentation.enable = false;
+
+    environment.etc = {
+      "ssh/ssh_host_ed25519_key" = {
+        mode = "0600";
+
+        source = ./keys/ssh_host_ed25519_key;
+      };
+
+      "ssh/ssh_host_ed25519_key.pub" = {
+        mode = "0644";
+
+        source = ./keys/ssh_host_ed25519_key.pub;
+      };
+    };
+
+    # DNS fails for QEMU user networking (SLiRP) on macOS.  See:
+    #
+    # https://github.com/utmapp/UTM/issues/2353
+    #
+    # This works around that by using a public DNS server other than the DNS
+    # server that QEMU provides (normally 10.0.2.3)
+    networking.nameservers = [ "8.8.8.8" ];
+
+    nix.settings = {
+      auto-optimise-store = true;
+
+      min-free = cfg.min-free;
+
+      max-free = cfg.max-free;
+
+      trusted-users = [ "root" user ];
+    };
+
+    services = {
+      getty.autologinUser = user;
+
+      openssh = {
+        enable = true;
+
+        authorizedKeysFiles = [ "${keysDirectory}/%u_${keyType}.pub" ];
+      };
+    };
+
+    system.build.macos-builder-installer =
+      let
+        privateKey = "/etc/nix/${user}_${keyType}";
+
+        publicKey = "${privateKey}.pub";
+
+        # This installCredentials script is written so that it's as easy as
+        # possible for a user to audit before confirming the `sudo`
+        installCredentials = hostPkgs.writeShellScript "install-credentials" ''
+          KEYS="''${1}"
+          INSTALL=${hostPkgs.coreutils}/bin/install
+          "''${INSTALL}" -g nixbld -m 600 "''${KEYS}/${user}_${keyType}" ${privateKey}
+          "''${INSTALL}" -g nixbld -m 644 "''${KEYS}/${user}_${keyType}.pub" ${publicKey}
+        '';
+
+        hostPkgs = config.virtualisation.host.pkgs;
+
+  script = hostPkgs.writeShellScriptBin "create-builder" (
+          # When running as non-interactively as part of a DarwinConfiguration the working directory
+          # must be set to a writeable directory.
+        (if cfg.workingDirectory != "." then ''
+          ${hostPkgs.coreutils}/bin/mkdir --parent "${cfg.workingDirectory}"
+          cd "${cfg.workingDirectory}"
+  '' else "") + ''
+          KEYS="''${KEYS:-./keys}"
+          ${hostPkgs.coreutils}/bin/mkdir --parent "''${KEYS}"
+          PRIVATE_KEY="''${KEYS}/${user}_${keyType}"
+          PUBLIC_KEY="''${PRIVATE_KEY}.pub"
+          if [ ! -e "''${PRIVATE_KEY}" ] || [ ! -e "''${PUBLIC_KEY}" ]; then
+              ${hostPkgs.coreutils}/bin/rm --force -- "''${PRIVATE_KEY}" "''${PUBLIC_KEY}"
+              ${hostPkgs.openssh}/bin/ssh-keygen -q -f "''${PRIVATE_KEY}" -t ${keyType} -N "" -C 'builder@localhost'
+          fi
+          if ! ${hostPkgs.diffutils}/bin/cmp "''${PUBLIC_KEY}" ${publicKey}; then
+            (set -x; sudo --reset-timestamp ${installCredentials} "''${KEYS}")
+          fi
+          KEYS="$(${hostPkgs.nix}/bin/nix-store --add "$KEYS")" ${config.system.build.vm}/bin/run-nixos-vm
+        '');
+
+      in
+      script.overrideAttrs (old: {
+        meta = (old.meta or { }) // {
+          platforms = lib.platforms.darwin;
+        };
+      });
+
+    system = {
+      # To prevent gratuitous rebuilds on each change to Nixpkgs
+      nixos.revision = null;
+
+      stateVersion = lib.mkDefault (throw ''
+        The macOS linux builder should not need a stateVersion to be set, but a module
+        has accessed stateVersion nonetheless.
+        Please inspect the trace of the following command to figure out which module
+        has a dependency on stateVersion.
+
+          nix-instantiate --attr darwin.builder --show-trace
+      '');
+    };
+
+    users.users."${user}" = {
+      isNormalUser = true;
+    };
+
+    security.polkit.enable = true;
+
+    security.polkit.extraConfig = ''
+      polkit.addRule(function(action, subject) {
+        if (action.id === "org.freedesktop.login1.power-off" && subject.user === "${user}") {
+          return "yes";
+        } else {
+          return "no";
+        }
+      })
+    '';
+
+    virtualisation = {
+      diskSize = cfg.diskSize;
+
+      memorySize = cfg.memorySize;
+
+      forwardPorts = [
+        { from = "host"; guest.port = 22; host.port = cfg.hostPort; }
+      ];
+
+      # Disable graphics for the builder since users will likely want to run it
+      # non-interactively in the background.
+      graphics = false;
+
+      sharedDirectories.keys = {
+        source = "\"$KEYS\"";
+        target = keysDirectory;
+      };
+
+      # If we don't enable this option then the host will fail to delegate builds
+      # to the guest, because:
+      #
+      # - The host will lock the path to build
+      # - The host will delegate the build to the guest
+      # - The guest will attempt to lock the same path and fail because
+      #   the lockfile on the host is visible on the guest
+      #
+      # Snapshotting the host's /nix/store as an image isolates the guest VM's
+      # /nix/store from the host's /nix/store, preventing this problem.
+      useNixStoreImage = true;
+
+      # Obviously the /nix/store needs to be writable on the guest in order for it
+      # to perform builds.
+      writableStore = true;
+
+      # This ensures that anything built on the guest isn't lost when the guest is
+      # restarted.
+      writableStoreUseTmpfs = false;
+    };
+  };
+}
diff --git a/nixos/modules/programs/_1password-gui.nix b/nixos/modules/programs/_1password-gui.nix
index 83ef6037fb5..27c0d34a2ee 100644
--- a/nixos/modules/programs/_1password-gui.nix
+++ b/nixos/modules/programs/_1password-gui.nix
@@ -27,7 +27,7 @@ in
         '';
       };
 
-      package = mkPackageOption pkgs "1Password GUI" {
+      package = mkPackageOptionMD pkgs "1Password GUI" {
         default = [ "_1password-gui" ];
       };
     };
diff --git a/nixos/modules/programs/_1password.nix b/nixos/modules/programs/_1password.nix
index 91246150755..8537484c7e6 100644
--- a/nixos/modules/programs/_1password.nix
+++ b/nixos/modules/programs/_1password.nix
@@ -18,7 +18,7 @@ in
     programs._1password = {
       enable = mkEnableOption (lib.mdDoc "the 1Password CLI tool");
 
-      package = mkPackageOption pkgs "1Password CLI" {
+      package = mkPackageOptionMD pkgs "1Password CLI" {
         default = [ "_1password" ];
       };
     };
diff --git a/nixos/modules/programs/atop.nix b/nixos/modules/programs/atop.nix
index 2b14d7c7343..9d5843bd670 100644
--- a/nixos/modules/programs/atop.nix
+++ b/nixos/modules/programs/atop.nix
@@ -142,6 +142,7 @@ in
               # convert remainings logs and start eventually
               atop.serviceConfig.ExecStartPre = pkgs.writeShellScript "atop-update-log-format" ''
                 set -e -u
+                shopt -s nullglob
                 for logfile in "$LOGPATH"/atop_*
                 do
                   ${atop}/bin/atopconvert "$logfile" "$logfile".new
@@ -150,6 +151,8 @@ in
                   if ! ${pkgs.diffutils}/bin/cmp -s "$logfile" "$logfile".new
                   then
                     ${pkgs.coreutils}/bin/mv -v -f "$logfile".new "$logfile"
+                  else
+                    ${pkgs.coreutils}/bin/rm -f "$logfile".new
                   fi
                 done
               '';
diff --git a/nixos/modules/programs/ccache.nix b/nixos/modules/programs/ccache.nix
index 19fb7ca3294..567c853e8c7 100644
--- a/nixos/modules/programs/ccache.nix
+++ b/nixos/modules/programs/ccache.nix
@@ -17,7 +17,7 @@ in {
       type = types.listOf types.str;
       description = lib.mdDoc "Nix top-level packages to be compiled using CCache";
       default = [];
-      example = [ "wxGTK30" "ffmpeg" "libav_all" ];
+      example = [ "wxGTK32" "ffmpeg" "libav_all" ];
     };
   };
 
diff --git a/nixos/modules/programs/clash-verge.nix b/nixos/modules/programs/clash-verge.nix
new file mode 100644
index 00000000000..29977be3858
--- /dev/null
+++ b/nixos/modules/programs/clash-verge.nix
@@ -0,0 +1,41 @@
+{ config, lib, pkgs, ... }:
+
+{
+  options.programs.clash-verge = {
+    enable = lib.mkEnableOption (lib.mdDoc ''
+      Clash Verge.
+    '');
+
+    autoStart = lib.mkEnableOption (lib.mdDoc ''
+      Clash Verge Auto Launch.
+    '');
+
+    tunMode = lib.mkEnableOption (lib.mdDoc ''
+      Clash Verge Tun Mode.
+    '');
+  };
+
+  config =
+    let
+      cfg = config.programs.clash-verge;
+    in
+    lib.mkIf cfg.enable {
+
+      environment.systemPackages = [
+        pkgs.clash-verge
+        (lib.mkIf cfg.autoStart (pkgs.makeAutostartItem {
+          name = "clash-verge";
+          package = pkgs.clash-verge;
+        }))
+      ];
+
+      security.wrappers.clash-verge = lib.mkIf cfg.tunMode {
+        owner = "root";
+        group = "root";
+        capabilities = "cap_net_bind_service,cap_net_admin=+ep";
+        source = "${lib.getExe pkgs.clash-verge}";
+      };
+    };
+
+  meta.maintainers = with lib.maintainers; [ zendo ];
+}
diff --git a/nixos/modules/programs/darling.nix b/nixos/modules/programs/darling.nix
new file mode 100644
index 00000000000..c4e1c73b5c2
--- /dev/null
+++ b/nixos/modules/programs/darling.nix
@@ -0,0 +1,21 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.darling;
+in {
+  options = {
+    programs.darling = {
+      enable = lib.mkEnableOption (lib.mdDoc "Darling, a Darwin/macOS compatibility layer for Linux");
+      package = lib.mkPackageOptionMD pkgs "darling" {};
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    security.wrappers.darling = {
+      source = lib.getExe cfg.package;
+      owner = "root";
+      group = "root";
+      setuid = true;
+    };
+  };
+}
diff --git a/nixos/modules/programs/digitalbitbox/default.md b/nixos/modules/programs/digitalbitbox/default.md
new file mode 100644
index 00000000000..9bca14e97ff
--- /dev/null
+++ b/nixos/modules/programs/digitalbitbox/default.md
@@ -0,0 +1,47 @@
+# Digital Bitbox {#module-programs-digitalbitbox}
+
+Digital Bitbox is a hardware wallet and second-factor authenticator.
+
+The `digitalbitbox` programs module may be installed by setting
+`programs.digitalbitbox` to `true` in a manner similar to
+```
+programs.digitalbitbox.enable = true;
+```
+and bundles the `digitalbitbox` package (see [](#sec-digitalbitbox-package)),
+which contains the `dbb-app` and `dbb-cli` binaries, along with the hardware
+module (see [](#sec-digitalbitbox-hardware-module)) which sets up the necessary
+udev rules to access the device.
+
+Enabling the digitalbitbox module is pretty much the easiest way to get a
+Digital Bitbox device working on your system.
+
+For more information, see <https://digitalbitbox.com/start_linux>.
+
+## Package {#sec-digitalbitbox-package}
+
+The binaries, `dbb-app` (a GUI tool) and `dbb-cli` (a CLI tool), are available
+through the `digitalbitbox` package which could be installed as follows:
+```
+environment.systemPackages = [
+  pkgs.digitalbitbox
+];
+```
+
+## Hardware {#sec-digitalbitbox-hardware-module}
+
+The digitalbitbox hardware package enables the udev rules for Digital Bitbox
+devices and may be installed as follows:
+```
+hardware.digitalbitbox.enable = true;
+```
+
+In order to alter the udev rules, one may provide different values for the
+`udevRule51` and `udevRule52` attributes by means of overriding as follows:
+```
+programs.digitalbitbox = {
+  enable = true;
+  package = pkgs.digitalbitbox.override {
+    udevRule51 = "something else";
+  };
+};
+```
diff --git a/nixos/modules/programs/digitalbitbox/default.nix b/nixos/modules/programs/digitalbitbox/default.nix
index 101ee8ddbaf..5ee6cdafe63 100644
--- a/nixos/modules/programs/digitalbitbox/default.nix
+++ b/nixos/modules/programs/digitalbitbox/default.nix
@@ -33,7 +33,7 @@ in
   };
 
   meta = {
-    doc = ./doc.xml;
+    doc = ./default.md;
     maintainers = with lib.maintainers; [ vidbina ];
   };
 }
diff --git a/nixos/modules/programs/digitalbitbox/doc.xml b/nixos/modules/programs/digitalbitbox/doc.xml
deleted file mode 100644
index c63201628db..00000000000
--- a/nixos/modules/programs/digitalbitbox/doc.xml
+++ /dev/null
@@ -1,74 +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-programs-digitalbitbox">
- <title>Digital Bitbox</title>
- <para>
-  Digital Bitbox is a hardware wallet and second-factor authenticator.
- </para>
- <para>
-  The <literal>digitalbitbox</literal> programs module may be installed by
-  setting <literal>programs.digitalbitbox</literal> to <literal>true</literal>
-  in a manner similar to
-<programlisting>
-<xref linkend="opt-programs.digitalbitbox.enable"/> = true;
-</programlisting>
-  and bundles the <literal>digitalbitbox</literal> package (see
-  <xref
-      linkend="sec-digitalbitbox-package" />), which contains the
-  <literal>dbb-app</literal> and <literal>dbb-cli</literal> binaries, along
-  with the hardware module (see
-  <xref
-      linkend="sec-digitalbitbox-hardware-module" />) which sets up the
-  necessary udev rules to access the device.
- </para>
- <para>
-  Enabling the digitalbitbox module is pretty much the easiest way to get a
-  Digital Bitbox device working on your system.
- </para>
- <para>
-  For more information, see
-  <link xlink:href="https://digitalbitbox.com/start_linux" />.
- </para>
- <section xml:id="sec-digitalbitbox-package">
-  <title>Package</title>
-
-  <para>
-   The binaries, <literal>dbb-app</literal> (a GUI tool) and
-   <literal>dbb-cli</literal> (a CLI tool), are available through the
-   <literal>digitalbitbox</literal> package which could be installed as
-   follows:
-<programlisting>
-<xref linkend="opt-environment.systemPackages"/> = [
-  pkgs.digitalbitbox
-];
-</programlisting>
-  </para>
- </section>
- <section xml:id="sec-digitalbitbox-hardware-module">
-  <title>Hardware</title>
-
-  <para>
-   The digitalbitbox hardware package enables the udev rules for Digital Bitbox
-   devices and may be installed as follows:
-<programlisting>
-<xref linkend="opt-hardware.digitalbitbox.enable"/> = true;
-</programlisting>
-  </para>
-
-  <para>
-   In order to alter the udev rules, one may provide different values for the
-   <literal>udevRule51</literal> and <literal>udevRule52</literal> attributes
-   by means of overriding as follows:
-<programlisting>
-programs.digitalbitbox = {
-  <link linkend="opt-programs.digitalbitbox.enable">enable</link> = true;
-  <link linkend="opt-programs.digitalbitbox.package">package</link> = pkgs.digitalbitbox.override {
-    udevRule51 = "something else";
-  };
-};
-</programlisting>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/programs/firefox.nix b/nixos/modules/programs/firefox.nix
index 3a5105c57d7..ead048134d8 100644
--- a/nixos/modules/programs/firefox.nix
+++ b/nixos/modules/programs/firefox.nix
@@ -201,6 +201,7 @@ in
     nativeMessagingHosts = mapAttrs (_: v: mkEnableOption (mdDoc v)) {
       browserpass = "Browserpass support";
       bukubrow = "Bukubrow support";
+      euwebid = "Web eID support";
       ff2mpv = "ff2mpv support";
       fxCast = "fx_cast support";
       gsconnect = "GSConnect support";
@@ -217,6 +218,8 @@ in
         extraPrefs = cfg.autoConfig;
         extraNativeMessagingHosts = with pkgs; optionals nmh.ff2mpv [
           ff2mpv
+        ] ++ optionals nmh.euwebid [
+          web-eid-app
         ] ++ optionals nmh.gsconnect [
           gnomeExtensions.gsconnect
         ] ++ optionals nmh.jabref [
@@ -230,6 +233,7 @@ in
     nixpkgs.config.firefox = {
       enableBrowserpass = nmh.browserpass;
       enableBukubrow = nmh.bukubrow;
+      enableEUWebID = nmh.euwebid;
       enableTridactylNative = nmh.tridactyl;
       enableUgetIntegrator = nmh.ugetIntegrator;
       enableFXCastBridge = nmh.fxCast;
diff --git a/nixos/modules/programs/flashrom.nix b/nixos/modules/programs/flashrom.nix
index ff495558c9e..9f8faff14e4 100644
--- a/nixos/modules/programs/flashrom.nix
+++ b/nixos/modules/programs/flashrom.nix
@@ -16,12 +16,11 @@ in
         group.
       '';
     };
-    package = mkPackageOption pkgs "flashrom" { };
+    package = mkPackageOptionMD pkgs "flashrom" { };
   };
 
   config = mkIf cfg.enable {
     services.udev.packages = [ cfg.package ];
     environment.systemPackages = [ cfg.package ];
-    users.groups.flashrom = { };
   };
 }
diff --git a/nixos/modules/programs/fzf.nix b/nixos/modules/programs/fzf.nix
index eda4eacde4a..4442d88941c 100644
--- a/nixos/modules/programs/fzf.nix
+++ b/nixos/modules/programs/fzf.nix
@@ -1,8 +1,9 @@
-{pkgs, config, lib, ...}:
+{ pkgs, config, lib, ... }:
 with lib;
 let
   cfg = config.programs.fzf;
-in {
+in
+{
   options = {
     programs.fzf = {
       fuzzyCompletion = mkEnableOption (mdDoc "fuzzy completion with fzf");
@@ -11,17 +12,21 @@ in {
   };
   config = {
     environment.systemPackages = optional (cfg.keybindings || cfg.fuzzyCompletion) pkgs.fzf;
+
     programs.bash.interactiveShellInit = optionalString cfg.fuzzyCompletion ''
       source ${pkgs.fzf}/share/fzf/completion.bash
     '' + optionalString cfg.keybindings ''
       source ${pkgs.fzf}/share/fzf/key-bindings.bash
     '';
 
-    programs.zsh.interactiveShellInit = optionalString cfg.fuzzyCompletion ''
-      source ${pkgs.fzf}/share/fzf/completion.zsh
-    '' + optionalString cfg.keybindings ''
-      source ${pkgs.fzf}/share/fzf/key-bindings.zsh
-    '';
+    programs.zsh.interactiveShellInit = optionalString (!config.programs.zsh.ohMyZsh.enable)
+      (optionalString cfg.fuzzyCompletion ''
+        source ${pkgs.fzf}/share/fzf/completion.zsh
+      '' + optionalString cfg.keybindings ''
+        source ${pkgs.fzf}/share/fzf/key-bindings.zsh
+      '');
+
+    programs.zsh.ohMyZsh.plugins = optional (cfg.keybindings || cfg.fuzzyCompletion) [ "fzf" ];
   };
   meta.maintainers = with maintainers; [ laalsaas ];
 }
diff --git a/nixos/modules/programs/gamescope.nix b/nixos/modules/programs/gamescope.nix
new file mode 100644
index 00000000000..c4424849a41
--- /dev/null
+++ b/nixos/modules/programs/gamescope.nix
@@ -0,0 +1,85 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+with lib; let
+  cfg = config.programs.gamescope;
+
+  gamescope =
+    let
+      wrapperArgs =
+        optional (cfg.args != [ ])
+          ''--add-flags "${toString cfg.args}"''
+        ++ builtins.attrValues (mapAttrs (var: val: "--set-default ${var} ${val}") cfg.env);
+    in
+    pkgs.runCommand "gamescope" { nativeBuildInputs = [ pkgs.makeBinaryWrapper ]; } ''
+      mkdir -p $out/bin
+      makeWrapper ${cfg.package}/bin/gamescope $out/bin/gamescope --inherit-argv0 \
+        ${toString wrapperArgs}
+    '';
+in
+{
+  options.programs.gamescope = {
+    enable = mkEnableOption (mdDoc "gamescope");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.gamescope;
+      defaultText = literalExpression "pkgs.gamescope";
+      description = mdDoc ''
+        The GameScope package to use.
+      '';
+    };
+
+    capSysNice = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc ''
+        Add cap_sys_nice capability to the GameScope
+        binary so that it may renice itself.
+      '';
+    };
+
+    args = mkOption {
+      type = types.listOf types.string;
+      default = [ ];
+      example = [ "--rt" "--prefer-vk-device 8086:9bc4" ];
+      description = mdDoc ''
+        Arguments passed to GameScope on startup.
+      '';
+    };
+
+    env = mkOption {
+      type = types.attrsOf types.string;
+      default = { };
+      example = literalExpression ''
+        # for Prime render offload on Nvidia laptops.
+        # Also requires `hardware.nvidia.prime.offload.enable`.
+        {
+          __NV_PRIME_RENDER_OFFLOAD = "1";
+          __VK_LAYER_NV_optimus = "NVIDIA_only";
+          __GLX_VENDOR_LIBRARY_NAME = "nvidia";
+        }
+      '';
+      description = mdDoc ''
+        Default environment variables available to the GameScope process, overridable at runtime.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    security.wrappers = mkIf cfg.capSysNice {
+      gamescope = {
+        owner = "root";
+        group = "root";
+        source = "${gamescope}/bin/gamescope";
+        capabilities = "cap_sys_nice+pie";
+      };
+    };
+
+    environment.systemPackages = mkIf (!cfg.capSysNice) [ gamescope ];
+  };
+
+  meta.maintainers = with maintainers; [ nrdxp ];
+}
diff --git a/nixos/modules/programs/git.nix b/nixos/modules/programs/git.nix
index acff5dfdd88..4e271a8c134 100644
--- a/nixos/modules/programs/git.nix
+++ b/nixos/modules/programs/git.nix
@@ -20,15 +20,41 @@ in
       };
 
       config = mkOption {
-        type = with types; attrsOf (attrsOf anything);
-        default = { };
+        type =
+          with types;
+          let
+            gitini = attrsOf (attrsOf anything);
+          in
+          either gitini (listOf gitini) // {
+            merge = loc: defs:
+              let
+                config = foldl'
+                  (acc: { value, ... }@x: acc // (if isList value then {
+                    ordered = acc.ordered ++ value;
+                  } else {
+                    unordered = acc.unordered ++ [ x ];
+                  }))
+                  {
+                    ordered = [ ];
+                    unordered = [ ];
+                  }
+                  defs;
+              in
+              [ (gitini.merge loc config.unordered) ] ++ config.ordered;
+          };
+        default = [ ];
         example = {
           init.defaultBranch = "main";
           url."https://github.com/".insteadOf = [ "gh:" "github:" ];
         };
         description = lib.mdDoc ''
-          Configuration to write to /etc/gitconfig. See the CONFIGURATION FILE
-          section of git-config(1) for more information.
+          Configuration to write to /etc/gitconfig. A list can also be
+          specified to keep the configuration in order. For example, setting
+          `config` to `[ { foo.x = 42; } { bar.y = 42; }]` will put the `foo`
+          section before the `bar` section unlike the default alphabetical
+          order, which can be helpful for sections such as `include` and
+          `includeIf`. See the CONFIGURATION FILE section of git-config(1) for
+          more information.
         '';
       };
 
@@ -48,8 +74,8 @@ in
   config = mkMerge [
     (mkIf cfg.enable {
       environment.systemPackages = [ cfg.package ];
-      environment.etc.gitconfig = mkIf (cfg.config != {}) {
-        text = generators.toGitINI cfg.config;
+      environment.etc.gitconfig = mkIf (cfg.config != [ ]) {
+        text = concatMapStringsSep "\n" generators.toGitINI cfg.config;
       };
     })
     (mkIf (cfg.enable && cfg.lfs.enable) {
diff --git a/nixos/modules/programs/gnome-documents.nix b/nixos/modules/programs/gnome-documents.nix
deleted file mode 100644
index 2831ac9aff2..00000000000
--- a/nixos/modules/programs/gnome-documents.nix
+++ /dev/null
@@ -1,54 +0,0 @@
-# GNOME Documents.
-
-{ config, pkgs, lib, ... }:
-
-with lib;
-
-{
-
-  meta = {
-    maintainers = teams.gnome.members;
-  };
-
-  # Added 2019-08-09
-  imports = [
-    (mkRenamedOptionModule
-      [ "services" "gnome" "gnome-documents" "enable" ]
-      [ "programs" "gnome-documents" "enable" ])
-  ];
-
-  ###### interface
-
-  options = {
-
-    programs.gnome-documents = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to enable GNOME Documents, a document
-          manager application for GNOME.
-        '';
-      };
-
-    };
-
-  };
-
-
-  ###### implementation
-
-  config = mkIf config.programs.gnome-documents.enable {
-
-    environment.systemPackages = [ pkgs.gnome.gnome-documents ];
-
-    services.dbus.packages = [ pkgs.gnome.gnome-documents ];
-
-    services.gnome.gnome-online-accounts.enable = true;
-
-    services.gnome.gnome-online-miners.enable = true;
-
-  };
-
-}
diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix
index 828f24f9911..cb8d0ecff4c 100644
--- a/nixos/modules/programs/gnupg.nix
+++ b/nixos/modules/programs/gnupg.nix
@@ -135,7 +135,7 @@ in
       # The SSH agent protocol doesn't have support for changing TTYs; however we
       # can simulate this with the `exec` feature of openssh (see ssh_config(5))
       # that hooks a command to the shell currently running the ssh program.
-      Match host * exec "${cfg.package}/bin/gpg-connect-agent --quiet updatestartuptty /bye >/dev/null 2>&1"
+      Match host * exec "${pkgs.runtimeShell} -c '${cfg.package}/bin/gpg-connect-agent --quiet updatestartuptty /bye >/dev/null 2>&1'"
     '';
 
     environment.extraInit = mkIf cfg.agent.enableSSHSupport ''
diff --git a/nixos/modules/programs/hyprland.nix b/nixos/modules/programs/hyprland.nix
new file mode 100644
index 00000000000..92b8e992e64
--- /dev/null
+++ b/nixos/modules/programs/hyprland.nix
@@ -0,0 +1,81 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+with lib; let
+  cfg = config.programs.hyprland;
+
+  defaultHyprlandPackage = pkgs.hyprland.override {
+    enableXWayland = cfg.xwayland.enable;
+    hidpiXWayland = cfg.xwayland.hidpi;
+    nvidiaPatches = cfg.nvidiaPatches;
+  };
+in
+{
+  options.programs.hyprland = {
+    enable = mkEnableOption null // {
+      description = mdDoc ''
+        Hyprland, the dynamic tiling Wayland compositor that doesn't sacrifice on its looks.
+
+        You can manually launch Hyprland by executing {command}`Hyprland` on a TTY.
+
+        A configuration file will be generated in {file}`~/.config/hypr/hyprland.conf`.
+        See <https://wiki.hyprland.org> for more information.
+      '';
+    };
+
+    package = mkOption {
+      type = types.path;
+      default = defaultHyprlandPackage;
+      defaultText = literalExpression ''
+        pkgs.hyprland.override {
+          enableXWayland = config.programs.hyprland.xwayland.enable;
+          hidpiXWayland = config.programs.hyprland.xwayland.hidpi;
+          nvidiaPatches = config.programs.hyprland.nvidiaPatches;
+        }
+      '';
+      example = literalExpression "<Hyprland flake>.packages.<system>.default";
+      description = mdDoc ''
+        The Hyprland package to use.
+        Setting this option will make {option}`programs.hyprland.xwayland` and
+        {option}`programs.hyprland.nvidiaPatches` not work.
+      '';
+    };
+
+    xwayland = {
+      enable = mkEnableOption (mdDoc "XWayland") // { default = true; };
+      hidpi = mkEnableOption null // {
+        description = mdDoc ''
+          Enable HiDPI XWayland, based on [XWayland MR 733](https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/733).
+          See <https://wiki.hyprland.org/Nix/Options-Overrides/#xwayland-hidpi> for more info.
+        '';
+      };
+    };
+
+    nvidiaPatches = mkEnableOption (mdDoc "patching wlroots for better Nvidia support");
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    fonts.enableDefaultFonts = mkDefault true;
+    hardware.opengl.enable = mkDefault true;
+
+    programs = {
+      dconf.enable = mkDefault true;
+      xwayland.enable = mkDefault cfg.xwayland.enable;
+    };
+
+    security.polkit.enable = true;
+
+    services.xserver.displayManager.sessionPackages = [ cfg.package ];
+
+    xdg.portal = {
+      enable = mkDefault true;
+      extraPortals = [
+        pkgs.xdg-desktop-portal-hyprland
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/programs/i3lock.nix b/nixos/modules/programs/i3lock.nix
new file mode 100644
index 00000000000..466ae59c927
--- /dev/null
+++ b/nixos/modules/programs/i3lock.nix
@@ -0,0 +1,58 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.programs.i3lock;
+
+in {
+
+  ###### interface
+
+  options = {
+    programs.i3lock = {
+      enable = mkEnableOption (mdDoc "i3lock");
+      package = mkOption {
+        type        = types.package;
+        default     = pkgs.i3lock;
+        defaultText = literalExpression "pkgs.i3lock";
+        example     = literalExpression ''
+          pkgs.i3lock-color
+        '';
+        description = mdDoc ''
+          Specify which package to use for the i3lock program,
+          The i3lock package must include a i3lock file or link in its out directory in order for the u2fSupport option to work correctly.
+        '';
+      };
+      u2fSupport = mkOption {
+        type        = types.bool;
+        default     = false;
+        example     = true;
+        description = mdDoc ''
+          Whether to enable U2F support in the i3lock program.
+          U2F enables authentication using a hardware device, such as a security key.
+          When U2F support is enabled, the i3lock program will set the setuid bit on the i3lock binary and enable the pam u2fAuth service,
+        '';
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ cfg.package ];
+
+    security.wrappers.i3lock = mkIf cfg.u2fSupport {
+      setuid = true;
+      owner = "root";
+      group = "root";
+      source = "${cfg.package.out}/bin/i3lock";
+    };
+
+    security.pam.services.i3lock.u2fAuth = cfg.u2fSupport;
+
+  };
+
+}
diff --git a/nixos/modules/programs/iay.nix b/nixos/modules/programs/iay.nix
new file mode 100644
index 00000000000..9164f5cb648
--- /dev/null
+++ b/nixos/modules/programs/iay.nix
@@ -0,0 +1,37 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.iay;
+  inherit (lib) mkEnableOption mkIf mkOption mkPackageOptionMD optionalString types;
+in {
+  options.programs.iay = {
+    enable = mkEnableOption (lib.mdDoc "iay");
+    package = mkPackageOptionMD pkgs "iay" {};
+
+    minimalPrompt = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc "Use minimal one-liner prompt.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    programs.bash.promptInit = ''
+      if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then
+        PS1='$(iay ${optionalString cfg.minimalPrompt "-m"})'
+      fi
+    '';
+
+    programs.zsh.promptInit = ''
+      if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then
+        autoload -Uz add-zsh-hook
+        _iay_prompt() {
+          PROMPT="$(iay -z ${optionalString cfg.minimalPrompt "-m"})"
+        }
+        add-zsh-hook precmd _iay_prompt
+      fi
+    '';
+  };
+
+  meta.maintainers = pkgs.iay.meta.maintainers;
+}
diff --git a/nixos/modules/programs/java.nix b/nixos/modules/programs/java.nix
index 4f03c1f3ff2..c5f83858d06 100644
--- a/nixos/modules/programs/java.nix
+++ b/nixos/modules/programs/java.nix
@@ -8,7 +8,6 @@ with lib;
 let
   cfg = config.programs.java;
 in
-
 {
 
   options = {
@@ -40,12 +39,35 @@ in
         type = types.package;
       };
 
+      binfmt = mkEnableOption (lib.mdDoc "binfmt to execute java jar's and classes");
+
     };
 
   };
 
   config = mkIf cfg.enable {
 
+    boot.binfmt.registrations = mkIf cfg.binfmt {
+      java-class = {
+        recognitionType = "extension";
+        magicOrExtension = "class";
+        interpreter = pkgs.writeShellScript "java-class-wrapper" ''
+          test -e ${cfg.package}/nix-support/setup-hook && source ${cfg.package}/nix-support/setup-hook
+          classpath=$(dirname "$1")
+          class=$(basename "''${1%%.class}")
+          $JAVA_HOME/bin/java -classpath "$classpath" "$class" "''${@:2}"
+        '';
+      };
+      java-jar = {
+        recognitionType = "extension";
+        magicOrExtension = "jar";
+        interpreter = pkgs.writeShellScript "java-jar-wrapper" ''
+          test -e ${cfg.package}/nix-support/setup-hook && source ${cfg.package}/nix-support/setup-hook
+          $JAVA_HOME/bin/java -jar "$@"
+        '';
+      };
+    };
+
     environment.systemPackages = [ cfg.package ];
 
     environment.shellInit = ''
diff --git a/nixos/modules/programs/k3b.nix b/nixos/modules/programs/k3b.nix
index cdaed3cf70f..5d19e4f1cc4 100644
--- a/nixos/modules/programs/k3b.nix
+++ b/nixos/modules/programs/k3b.nix
@@ -28,7 +28,7 @@ with lib;
       k3b
       dvdplusrwtools
       cdrdao
-      cdrkit
+      cdrtools
     ];
 
     security.wrappers = {
@@ -44,7 +44,7 @@ with lib;
         owner = "root";
         group = "cdrom";
         permissions = "u+wrx,g+x";
-        source = "${pkgs.cdrkit}/bin/cdrecord";
+        source = "${pkgs.cdrtools}/bin/cdrecord";
       };
     };
 
diff --git a/nixos/modules/programs/less.nix b/nixos/modules/programs/less.nix
index a1134e77436..81c68307aee 100644
--- a/nixos/modules/programs/less.nix
+++ b/nixos/modules/programs/less.nix
@@ -11,7 +11,7 @@ let
     ${concatStringsSep "\n"
       (mapAttrsToList (command: action: "${command} ${action}") cfg.commands)
     }
-    ${if cfg.clearDefaultCommands then "#stop" else ""}
+    ${optionalString cfg.clearDefaultCommands "#stop"}
 
     #line-edit
     ${concatStringsSep "\n"
diff --git a/nixos/modules/programs/minipro.nix b/nixos/modules/programs/minipro.nix
new file mode 100644
index 00000000000..a947f83f2ee
--- /dev/null
+++ b/nixos/modules/programs/minipro.nix
@@ -0,0 +1,29 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.minipro;
+in
+{
+  options = {
+    programs.minipro = {
+      enable = lib.mkEnableOption (lib.mdDoc "minipro") // {
+        description = lib.mdDoc ''
+          Installs minipro and its udev rules.
+          Users of the `plugdev` group can interact with connected MiniPRO chip programmers.
+        '';
+      };
+
+      package = lib.mkPackageOptionMD pkgs "minipro" { };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    users.groups.plugdev = { };
+    environment.systemPackages = [ cfg.package ];
+    services.udev.packages = [ cfg.package ];
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ infinidoge ];
+  };
+}
diff --git a/nixos/modules/programs/miriway.nix b/nixos/modules/programs/miriway.nix
new file mode 100644
index 00000000000..a67e1a17a7e
--- /dev/null
+++ b/nixos/modules/programs/miriway.nix
@@ -0,0 +1,78 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.programs.miriway;
+in {
+  options.programs.miriway = {
+    enable = lib.mkEnableOption (lib.mdDoc ''
+      Miriway, a Mir based Wayland compositor. You can manually launch Miriway by
+      executing "exec miriway" on a TTY, or launch it from a display manager. Copy
+      /etc/xdg/xdg-miriway/miriway-shell.config to ~/.config/miriway-shell.config
+      to modify the system-wide configuration on a per-user basis. See <https://github.com/Miriway/Miriway>,
+      and "miriway --help" for more information'');
+
+    config = lib.mkOption {
+      type = lib.types.lines;
+      default = ''
+        x11-window-title=Miriway (Mir-on-X)
+        idle-timeout=600
+        ctrl-alt=t:miriway-terminal # Default "terminal emulator finder"
+
+        shell-component=dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY
+
+        meta=Left:@dock-left
+        meta=Right:@dock-right
+        meta=Space:@toggle-maximized
+        meta=Home:@workspace-begin
+        meta=End:@workspace-end
+        meta=Page_Up:@workspace-up
+        meta=Page_Down:@workspace-down
+        ctrl-alt=BackSpace:@exit
+      '';
+      example = ''
+        idle-timeout=300
+        ctrl-alt=t:weston-terminal
+        add-wayland-extensions=all
+
+        shell-components=dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY
+
+        shell-component=waybar
+        shell-component=wbg Pictures/wallpaper
+
+        shell-meta=a:synapse
+
+        meta=Left:@dock-left
+        meta=Right:@dock-right
+        meta=Space:@toggle-maximized
+        meta=Home:@workspace-begin
+        meta=End:@workspace-end
+        meta=Page_Up:@workspace-up
+        meta=Page_Down:@workspace-down
+        ctrl-alt=BackSpace:@exit
+      '';
+      description = lib.mdDoc ''
+        Miriway's config. This will be installed system-wide.
+        The default will install the miriway package's barebones example config.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment = {
+      systemPackages = [ pkgs.miriway ];
+      etc = {
+        "xdg/xdg-miriway/miriway-shell.config".text = cfg.config;
+      };
+    };
+
+    hardware.opengl.enable = lib.mkDefault true;
+    fonts.enableDefaultFonts = lib.mkDefault true;
+    programs.dconf.enable = lib.mkDefault true;
+    programs.xwayland.enable = lib.mkDefault true;
+
+    # To make the Miriway session available if a display manager like SDDM is enabled:
+    services.xserver.displayManager.sessionPackages = [ pkgs.miriway ];
+  };
+
+  meta.maintainers = with lib.maintainers; [ OPNA2608 ];
+}
diff --git a/nixos/modules/programs/neovim.nix b/nixos/modules/programs/neovim.nix
index 8de527fceb2..3f0e9fc173b 100644
--- a/nixos/modules/programs/neovim.nix
+++ b/nixos/modules/programs/neovim.nix
@@ -4,12 +4,8 @@ with lib;
 
 let
   cfg = config.programs.neovim;
-
-  runtime' = filter (f: f.enable) (attrValues cfg.runtime);
-
-  runtime = pkgs.linkFarm "neovim-runtime" (map (x: { name = x.target; path = x.source; }) runtime');
-
-in {
+in
+{
   options.programs.neovim = {
     enable = mkOption {
       type = types.bool;
@@ -70,7 +66,7 @@ in {
 
     configure = mkOption {
       type = types.attrs;
-      default = {};
+      default = { };
       example = literalExpression ''
         {
           customRC = '''
@@ -105,7 +101,7 @@ in {
     };
 
     runtime = mkOption {
-      default = {};
+      default = { };
       example = literalExpression ''
         { "ftplugin/c.vim".text = "setlocal omnifunc=v:lua.vim.lsp.omnifunc"; }
       '';
@@ -115,14 +111,15 @@ in {
 
       type = with types; attrsOf (submodule (
         { name, config, ... }:
-        { options = {
+        {
+          options = {
 
             enable = mkOption {
               type = types.bool;
               default = true;
               description = lib.mdDoc ''
-                Whether this /etc file should be generated.  This
-                option allows specific /etc files to be disabled.
+                Whether this runtime directory should be generated.  This
+                option allows specific runtime files to be disabled.
               '';
             };
 
@@ -147,14 +144,9 @@ in {
 
           };
 
-          config = {
-            target = mkDefault name;
-            source = mkIf (config.text != null) (
-              let name' = "neovim-runtime" + baseNameOf name;
-              in mkDefault (pkgs.writeText name' config.text));
-          };
-
-        }));
+          config.target = mkDefault name;
+        }
+      ));
 
     };
   };
@@ -165,14 +157,17 @@ in {
     ];
     environment.variables.EDITOR = mkIf cfg.defaultEditor (mkOverride 900 "nvim");
 
-    programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package {
-      inherit (cfg) viAlias vimAlias withPython3 withNodeJs withRuby;
-      configure = cfg.configure // {
+    environment.etc = listToAttrs (attrValues (mapAttrs
+      (name: value: {
+        name = "xdg/nvim/${name}";
+        value = value // {
+          target = "xdg/nvim/${value.target}";
+        };
+      })
+      cfg.runtime));
 
-        customRC = (cfg.configure.customRC or "") + ''
-          set runtimepath^=${runtime}/etc
-        '';
-      };
+    programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package {
+      inherit (cfg) viAlias vimAlias withPython3 withNodeJs withRuby configure;
     };
   };
 }
diff --git a/nixos/modules/programs/nexttrace.nix b/nixos/modules/programs/nexttrace.nix
new file mode 100644
index 00000000000..091d4f17f9f
--- /dev/null
+++ b/nixos/modules/programs/nexttrace.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.programs.nexttrace;
+
+in
+{
+  options = {
+    programs.nexttrace = {
+      enable = lib.mkEnableOption (lib.mdDoc "Nexttrace to the global environment and configure a setcap wrapper for it");
+      package = lib.mkPackageOptionMD pkgs "nexttrace" { };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    security.wrappers.nexttrace = {
+      owner = "root";
+      group = "root";
+      capabilities = "cap_net_raw,cap_net_admin+eip";
+      source = "${cfg.package}/bin/nexttrace";
+    };
+  };
+}
diff --git a/nixos/modules/programs/nix-index.nix b/nixos/modules/programs/nix-index.nix
new file mode 100644
index 00000000000..a494b9d8c2c
--- /dev/null
+++ b/nixos/modules/programs/nix-index.nix
@@ -0,0 +1,62 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.programs.nix-index;
+in {
+  options.programs.nix-index = with lib; {
+    enable = mkEnableOption (lib.mdDoc "nix-index, a file database for nixpkgs");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.nix-index;
+      defaultText = literalExpression "pkgs.nix-index";
+      description = lib.mdDoc "Package providing the `nix-index` tool.";
+    };
+
+    enableBashIntegration = mkEnableOption (lib.mdDoc "Bash integration") // {
+      default = true;
+    };
+
+    enableZshIntegration = mkEnableOption (lib.mdDoc "Zsh integration") // {
+      default = true;
+    };
+
+    enableFishIntegration = mkEnableOption (lib.mdDoc "Fish integration") // {
+      default = true;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = let
+      checkOpt = name: {
+        assertion = cfg.${name} -> !config.programs.command-not-found.enable;
+        message = ''
+          The 'programs.command-not-found.enable' option is mutually exclusive
+          with the 'programs.nix-index.${name}' option.
+        '';
+      };
+    in [ (checkOpt "enableBashIntegration") (checkOpt "enableZshIntegration") ];
+
+    environment.systemPackages = [ cfg.package ];
+
+    programs.bash.interactiveShellInit = lib.mkIf cfg.enableBashIntegration ''
+      source ${cfg.package}/etc/profile.d/command-not-found.sh
+    '';
+
+    programs.zsh.interactiveShellInit = lib.mkIf cfg.enableZshIntegration ''
+      source ${cfg.package}/etc/profile.d/command-not-found.sh
+    '';
+
+    # See https://github.com/bennofs/nix-index/issues/126
+    programs.fish.interactiveShellInit = let
+      wrapper = pkgs.writeScript "command-not-found" ''
+        #!${pkgs.bash}/bin/bash
+        source ${cfg.package}/etc/profile.d/command-not-found.sh
+        command_not_found_handle "$@"
+      '';
+    in lib.mkIf cfg.enableFishIntegration ''
+      function __fish_command_not_found_handler --on-event fish_command_not_found
+          ${wrapper} $argv
+      end
+    '';
+  };
+}
diff --git a/nixos/modules/programs/nix-ld.nix b/nixos/modules/programs/nix-ld.nix
index f753cf5f97e..9a12b4ca5c7 100644
--- a/nixos/modules/programs/nix-ld.nix
+++ b/nixos/modules/programs/nix-ld.nix
@@ -36,23 +36,22 @@ let
 in
 {
   meta.maintainers = [ lib.maintainers.mic92 ];
-  options = {
-    programs.nix-ld = {
-      enable = lib.mkEnableOption (lib.mdDoc ''nix-ld, Documentation: <https://github.com/Mic92/nix-ld>'');
-      package = lib.mkOption {
-        type = lib.types.package;
-        description = lib.mdDoc "Which package to use for the nix-ld.";
-        default = pkgs.nix-ld;
-        defaultText = lib.mdDoc "pkgs.nix-ld";
-      };
-      libraries = lib.mkOption {
-        type = lib.types.listOf lib.types.package;
-        description = lib.mdDoc "Libraries that automatically become available to all programs. The default set includes common libraries.";
-        default = baseLibraries;
-        defaultText = lib.mdDoc "baseLibraries";
-      };
+  options.programs.nix-ld = {
+    enable = lib.mkEnableOption (lib.mdDoc ''nix-ld, Documentation: <https://github.com/Mic92/nix-ld>'');
+    package = lib.mkOption {
+      type = lib.types.package;
+      description = lib.mdDoc "Which package to use for the nix-ld.";
+      default = pkgs.nix-ld;
+      defaultText = lib.literalExpression "pkgs.nix-ld";
+    };
+    libraries = lib.mkOption {
+      type = lib.types.listOf lib.types.package;
+      description = lib.mdDoc "Libraries that automatically become available to all programs. The default set includes common libraries.";
+      default = baseLibraries;
+      defaultText = lib.literalExpression "baseLibraries derived from systemd and nix dependencies.";
     };
   };
+
   config = lib.mkIf config.programs.nix-ld.enable {
     systemd.tmpfiles.packages = [ cfg.package ];
 
diff --git a/nixos/modules/programs/plotinus.md b/nixos/modules/programs/plotinus.md
new file mode 100644
index 00000000000..fac3bbad1e0
--- /dev/null
+++ b/nixos/modules/programs/plotinus.md
@@ -0,0 +1,17 @@
+# Plotinus {#module-program-plotinus}
+
+*Source:* {file}`modules/programs/plotinus.nix`
+
+*Upstream documentation:* <https://github.com/p-e-w/plotinus>
+
+Plotinus is a searchable command palette in every modern GTK application.
+
+When in a GTK 3 application and Plotinus is enabled, you can press
+`Ctrl+Shift+P` to open the command palette. The command
+palette provides a searchable list of of all menu items in the application.
+
+To enable Plotinus, add the following to your
+{file}`configuration.nix`:
+```
+programs.plotinus.enable = true;
+```
diff --git a/nixos/modules/programs/plotinus.nix b/nixos/modules/programs/plotinus.nix
index a011bb862ae..c2b6884d649 100644
--- a/nixos/modules/programs/plotinus.nix
+++ b/nixos/modules/programs/plotinus.nix
@@ -8,7 +8,7 @@ in
 {
   meta = {
     maintainers = pkgs.plotinus.meta.maintainers;
-    doc = ./plotinus.xml;
+    doc = ./plotinus.md;
   };
 
   ###### interface
diff --git a/nixos/modules/programs/plotinus.xml b/nixos/modules/programs/plotinus.xml
deleted file mode 100644
index 8fc8c22c6d7..00000000000
--- a/nixos/modules/programs/plotinus.xml
+++ /dev/null
@@ -1,30 +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-program-plotinus">
- <title>Plotinus</title>
- <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/programs/plotinus.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://github.com/p-e-w/plotinus"/>
- </para>
- <para>
-  Plotinus is a searchable command palette in every modern GTK application.
- </para>
- <para>
-  When in a GTK 3 application and Plotinus is enabled, you can press
-  <literal>Ctrl+Shift+P</literal> to open the command palette. The command
-  palette provides a searchable list of of all menu items in the application.
- </para>
- <para>
-  To enable Plotinus, add the following to your
-  <filename>configuration.nix</filename>:
-<programlisting>
-<xref linkend="opt-programs.plotinus.enable"/> = true;
-</programlisting>
- </para>
-</chapter>
diff --git a/nixos/modules/programs/proxychains.nix b/nixos/modules/programs/proxychains.nix
index 0771f03c77d..9bdd5d40566 100644
--- a/nixos/modules/programs/proxychains.nix
+++ b/nixos/modules/programs/proxychains.nix
@@ -51,6 +51,10 @@ in {
 
       enable = mkEnableOption (lib.mdDoc "installing proxychains configuration");
 
+      package = mkPackageOptionMD pkgs "proxychains" {
+        example = "pkgs.proxychains-ng";
+      };
+
       chain = {
         type = mkOption {
           type = types.enum [ "dynamic" "strict" "random" ];
@@ -86,7 +90,7 @@ in {
         description = lib.mdDoc "Proxy DNS requests - no leak for DNS data.";
       };
 
-      quietMode = mkEnableOption (lib.mdDoc "Quiet mode (no output from the library).");
+      quietMode = mkEnableOption (lib.mdDoc "Quiet mode (no output from the library)");
 
       remoteDNSSubnet = mkOption {
         type = types.enum [ 10 127 224 ];
@@ -159,7 +163,7 @@ in {
       };
 
     environment.etc."proxychains.conf".text = configFile;
-    environment.systemPackages = [ pkgs.proxychains ];
+    environment.systemPackages = [ cfg.package ];
   };
 
 }
diff --git a/nixos/modules/programs/qdmr.nix b/nixos/modules/programs/qdmr.nix
new file mode 100644
index 00000000000..1bb81317bda
--- /dev/null
+++ b/nixos/modules/programs/qdmr.nix
@@ -0,0 +1,25 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+
+let
+  cfg = config.programs.qdmr;
+in {
+  meta.maintainers = [ lib.maintainers.janik ];
+
+  options = {
+    programs.qdmr = {
+      enable = lib.mkEnableOption (lib.mdDoc "QDMR - a GUI application and command line tool for programming DMR radios");
+      package = lib.mkPackageOptionMD pkgs "qdmr" { };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    services.udev.packages = [ cfg.package ];
+    users.groups.dialout = {};
+  };
+}
diff --git a/nixos/modules/programs/regreet.nix b/nixos/modules/programs/regreet.nix
new file mode 100644
index 00000000000..f6c750a45bf
--- /dev/null
+++ b/nixos/modules/programs/regreet.nix
@@ -0,0 +1,75 @@
+{ lib
+, pkgs
+, config
+, ...
+}:
+let
+  cfg = config.programs.regreet;
+  settingsFormat = pkgs.formats.toml { };
+in
+{
+  options.programs.regreet = {
+    enable = lib.mkEnableOption null // {
+      description = lib.mdDoc ''
+        Enable ReGreet, a clean and customizable greeter for greetd.
+
+        To use ReGreet, {option}`services.greetd` has to be enabled and
+        {option}`services.greetd.settings.default_session` should contain the
+        appropriate configuration to launch
+        {option}`config.programs.regreet.package`. For examples, see the
+        [ReGreet Readme](https://github.com/rharish101/ReGreet#set-as-default-session).
+
+        A minimal configuration that launches ReGreet in {command}`cage` is
+        enabled by this module by default.
+      '';
+    };
+
+    package = lib.mkPackageOptionMD pkgs [ "greetd" "regreet" ] { };
+
+    settings = lib.mkOption {
+      type = lib.types.either lib.types.path settingsFormat.type;
+      default = { };
+      description = lib.mdDoc ''
+        ReGreet configuration file. Refer
+        <https://github.com/rharish101/ReGreet/blob/main/regreet.sample.toml>
+        for options.
+      '';
+    };
+
+    extraCss = lib.mkOption {
+      type = lib.types.either lib.types.path lib.types.lines;
+      default = "";
+      description = lib.mdDoc ''
+        Extra CSS rules to apply on top of the GTK theme. Refer to
+        [GTK CSS Properties](https://docs.gtk.org/gtk4/css-properties.html) for
+        modifiable properties.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.greetd = {
+      enable = lib.mkDefault true;
+      settings.default_session.command = lib.mkDefault "${pkgs.dbus}/bin/dbus-run-session ${lib.getExe pkgs.cage} -s -- ${lib.getExe cfg.package}";
+    };
+
+    environment.etc = {
+      "greetd/regreet.css" =
+        if lib.isPath cfg.extraCss
+        then {source = cfg.extraCss;}
+        else {text = cfg.extraCss;};
+
+      "greetd/regreet.toml".source =
+        if lib.isPath cfg.settings
+        then cfg.settings
+        else settingsFormat.generate "regreet.toml" cfg.settings;
+    };
+
+    systemd.tmpfiles.rules = let
+      user = config.services.greetd.settings.default_session.user;
+    in [
+      "d /var/log/regreet 0755 greeter ${user} - -"
+      "d /var/cache/regreet 0755 greeter ${user} - -"
+    ];
+  };
+}
diff --git a/nixos/modules/programs/sharing.nix b/nixos/modules/programs/sharing.nix
new file mode 100644
index 00000000000..9ab51859dc5
--- /dev/null
+++ b/nixos/modules/programs/sharing.nix
@@ -0,0 +1,19 @@
+{ config, pkgs, lib, ... }:
+with lib;
+{
+  options.programs.sharing = {
+    enable = mkEnableOption (lib.mdDoc ''
+      sharing, a CLI tool for sharing files.
+
+      Note that it will opens the 7478 port for TCP in the firewall, which is needed for it to function properly
+    '');
+  };
+  config =
+    let
+      cfg = config.programs.sharing;
+    in
+      mkIf cfg.enable {
+        environment.systemPackages = [ pkgs.sharing ];
+        networking.firewall.allowedTCPPorts = [ 7478 ];
+      };
+}
diff --git a/nixos/modules/programs/singularity.nix b/nixos/modules/programs/singularity.nix
index 9648d0c2787..4884e5bdf2d 100644
--- a/nixos/modules/programs/singularity.nix
+++ b/nixos/modules/programs/singularity.nix
@@ -3,32 +3,90 @@
 with lib;
 let
   cfg = config.programs.singularity;
-  singularity = pkgs.singularity.overrideAttrs (attrs : {
-    installPhase = attrs.installPhase + ''
-      mv $out/libexec/singularity/bin/starter-suid $out/libexec/singularity/bin/starter-suid.orig
-      ln -s /run/wrappers/bin/singularity-suid $out/libexec/singularity/bin/starter-suid
-    '';
-  });
-in {
+in
+{
+
   options.programs.singularity = {
-    enable = mkEnableOption (lib.mdDoc "Singularity");
+    enable = mkEnableOption (mdDoc "singularity") // {
+      description = mdDoc ''
+        Whether to install Singularity/Apptainer with system-level overriding such as SUID support.
+      '';
+    };
+    package = mkOption {
+      type = types.package;
+      default = pkgs.singularity;
+      defaultText = literalExpression "pkgs.singularity";
+      example = literalExpression "pkgs.apptainer";
+      description = mdDoc ''
+        Singularity/Apptainer package to override and install.
+      '';
+    };
+    packageOverriden = mkOption {
+      type = types.nullOr types.package;
+      default = null;
+      description = mdDoc ''
+        This option provides access to the overriden result of `programs.singularity.package`.
+
+        For example, the following configuration makes all the Nixpkgs packages use the overriden `singularity`:
+        ```Nix
+        { config, lib, pkgs, ... }:
+        {
+          nixpkgs.overlays = [
+            (final: prev: {
+              _singularity-orig = prev.singularity;
+              singularity = config.programs.singularity.packageOverriden;
+            })
+          ];
+          programs.singularity.enable = true;
+          programs.singularity.package = pkgs._singularity-orig;
+        }
+        ```
+
+        Use `lib.mkForce` to forcefully specify the overriden package.
+      '';
+    };
+    enableFakeroot = mkOption {
+      type = types.bool;
+      default = true;
+      example = false;
+      description = mdDoc ''
+        Whether to enable the `--fakeroot` support of Singularity/Apptainer.
+      '';
+    };
+    enableSuid = mkOption {
+      type = types.bool;
+      default = true;
+      example = false;
+      description = mdDoc ''
+        Whether to enable the SUID support of Singularity/Apptainer.
+      '';
+    };
   };
 
   config = mkIf cfg.enable {
-      environment.systemPackages = [ singularity ];
-      security.wrappers.singularity-suid =
-      { setuid = true;
-        owner = "root";
-        group = "root";
-        source = "${singularity}/libexec/singularity/bin/starter-suid.orig";
-      };
-      systemd.tmpfiles.rules = [
-        "d /var/singularity/mnt/session 0770 root root -"
-        "d /var/singularity/mnt/final 0770 root root -"
-        "d /var/singularity/mnt/overlay 0770 root root -"
-        "d /var/singularity/mnt/container 0770 root root -"
-        "d /var/singularity/mnt/source 0770 root root -"
-      ];
+    programs.singularity.packageOverriden = (cfg.package.override (
+      optionalAttrs cfg.enableFakeroot {
+        newuidmapPath = "/run/wrappers/bin/newuidmap";
+        newgidmapPath = "/run/wrappers/bin/newgidmap";
+      } // optionalAttrs cfg.enableSuid {
+        enableSuid = true;
+        starterSuidPath = "/run/wrappers/bin/${cfg.package.projectName}-suid";
+      }
+    ));
+    environment.systemPackages = [ cfg.packageOverriden ];
+    security.wrappers."${cfg.packageOverriden.projectName}-suid" = mkIf cfg.enableSuid {
+      setuid = true;
+      owner = "root";
+      group = "root";
+      source = "${cfg.packageOverriden}/libexec/${cfg.packageOverriden.projectName}/bin/starter-suid.orig";
+    };
+    systemd.tmpfiles.rules = [
+      "d /var/lib/${cfg.packageOverriden.projectName}/mnt/session 0770 root root -"
+      "d /var/lib/${cfg.packageOverriden.projectName}/mnt/final 0770 root root -"
+      "d /var/lib/${cfg.packageOverriden.projectName}/mnt/overlay 0770 root root -"
+      "d /var/lib/${cfg.packageOverriden.projectName}/mnt/container 0770 root root -"
+      "d /var/lib/${cfg.packageOverriden.projectName}/mnt/source 0770 root root -"
+    ];
   };
 
 }
diff --git a/nixos/modules/programs/skim.nix b/nixos/modules/programs/skim.nix
index 57a5d68ec3d..8dadf322606 100644
--- a/nixos/modules/programs/skim.nix
+++ b/nixos/modules/programs/skim.nix
@@ -1,6 +1,6 @@
 { pkgs, config, lib, ... }:
 let
-  inherit (lib) mdDoc mkEnableOption mkPackageOption optional optionalString;
+  inherit (lib) mdDoc mkEnableOption mkPackageOptionMD optional optionalString;
   cfg = config.programs.skim;
 in
 {
@@ -8,7 +8,7 @@ in
     programs.skim = {
       fuzzyCompletion = mkEnableOption (mdDoc "fuzzy completion with skim");
       keybindings = mkEnableOption (mdDoc "skim keybindings");
-      package = mkPackageOption pkgs "skim" {};
+      package = mkPackageOptionMD pkgs "skim" {};
     };
   };
 
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
index 36b724e04bd..7c85d1e7c3d 100644
--- a/nixos/modules/programs/ssh.nix
+++ b/nixos/modules/programs/ssh.nix
@@ -26,7 +26,7 @@ let
       + (if h.publicKey != null then h.publicKey else readFile h.publicKeyFile)
     )) + "\n";
 
-  knownHostsFiles = [ "/etc/ssh/ssh_known_hosts" "/etc/ssh/ssh_known_hosts2" ]
+  knownHostsFiles = [ "/etc/ssh/ssh_known_hosts" ]
     ++ map pkgs.copyPathToStore cfg.knownHostsFiles;
 
 in
@@ -232,15 +232,14 @@ in
         description = lib.mdDoc ''
           Files containing SSH host keys to set as global known hosts.
           `/etc/ssh/ssh_known_hosts` (which is
-          generated by {option}`programs.ssh.knownHosts`) and
-          `/etc/ssh/ssh_known_hosts2` are always
-          included.
+          generated by {option}`programs.ssh.knownHosts`) is
+          always included.
         '';
         example = literalExpression ''
           [
             ./known_hosts
             (writeText "github.keys" '''
-              github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
+              github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
               github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
               github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
             ''')
@@ -282,7 +281,7 @@ in
   config = {
 
     programs.ssh.setXAuthLocation =
-      mkDefault (config.services.xserver.enable || config.programs.ssh.forwardX11 || config.services.openssh.forwardX11);
+      mkDefault (config.services.xserver.enable || config.programs.ssh.forwardX11 || config.services.openssh.settings.X11Forwarding);
 
     assertions =
       [ { assertion = cfg.forwardX11 -> cfg.setXAuthLocation;
diff --git a/nixos/modules/programs/starship.nix b/nixos/modules/programs/starship.nix
index b56c0b25616..cacad8eafe3 100644
--- a/nixos/modules/programs/starship.nix
+++ b/nixos/modules/programs/starship.nix
@@ -9,10 +9,27 @@ let
 
   settingsFile = settingsFormat.generate "starship.toml" cfg.settings;
 
-in {
+  initOption =
+    if cfg.interactiveOnly then
+      "promptInit"
+    else
+      "shellInit";
+
+in
+{
   options.programs.starship = {
     enable = mkEnableOption (lib.mdDoc "the Starship shell prompt");
 
+    interactiveOnly = mkOption {
+      default = true;
+      example = false;
+      type = types.bool;
+      description = lib.mdDoc ''
+        Whether to enable starship only when the shell is interactive.
+        Some plugins require this to be set to false to function correctly.
+      '';
+    };
+
     settings = mkOption {
       inherit (settingsFormat) type;
       default = { };
@@ -25,21 +42,21 @@ in {
   };
 
   config = mkIf cfg.enable {
-    programs.bash.promptInit = ''
+    programs.bash.${initOption} = ''
       if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then
         export STARSHIP_CONFIG=${settingsFile}
         eval "$(${pkgs.starship}/bin/starship init bash)"
       fi
     '';
 
-    programs.fish.promptInit = ''
+    programs.fish.${initOption} = ''
       if test "$TERM" != "dumb" -a \( -z "$INSIDE_EMACS" -o "$INSIDE_EMACS" = "vterm" \)
         set -x STARSHIP_CONFIG ${settingsFile}
         eval (${pkgs.starship}/bin/starship init fish)
       end
     '';
 
-    programs.zsh.promptInit = ''
+    programs.zsh.${initOption} = ''
       if [[ $TERM != "dumb" && (-z $INSIDE_EMACS || $INSIDE_EMACS == "vterm") ]]; then
         export STARSHIP_CONFIG=${settingsFile}
         eval "$(${pkgs.starship}/bin/starship init zsh)"
diff --git a/nixos/modules/programs/steam.nix b/nixos/modules/programs/steam.nix
index 1b69aac9886..c63b31bde11 100644
--- a/nixos/modules/programs/steam.nix
+++ b/nixos/modules/programs/steam.nix
@@ -4,28 +4,65 @@ with lib;
 
 let
   cfg = config.programs.steam;
+  gamescopeCfg = config.programs.gamescope;
+
+  steam-gamescope = let
+    exports = builtins.attrValues (builtins.mapAttrs (n: v: "export ${n}=${v}") cfg.gamescopeSession.env);
+  in
+    pkgs.writeShellScriptBin "steam-gamescope" ''
+      ${builtins.concatStringsSep "\n" exports}
+      gamescope --steam ${toString cfg.gamescopeSession.args} -- steam -tenfoot -pipewire-dmabuf
+    '';
+
+  gamescopeSessionFile =
+    (pkgs.writeTextDir "share/wayland-sessions/steam.desktop" ''
+      [Desktop Entry]
+      Name=Steam
+      Comment=A digital distribution platform
+      Exec=${steam-gamescope}/bin/steam-gamescope
+      Type=Application
+    '').overrideAttrs (_: { passthru.providedSessions = [ "steam" ]; });
 in {
   options.programs.steam = {
     enable = mkEnableOption (lib.mdDoc "steam");
 
     package = mkOption {
-      type        = types.package;
-      default     = pkgs.steam.override {
-        extraLibraries = pkgs: with config.hardware.opengl;
-          if pkgs.hostPlatform.is64bit
-          then [ package ] ++ extraPackages
-          else [ package32 ] ++ extraPackages32;
-      };
-      defaultText = literalExpression ''
-        pkgs.steam.override {
-          extraLibraries = pkgs: with config.hardware.opengl;
-            if pkgs.hostPlatform.is64bit
-            then [ package ] ++ extraPackages
-            else [ package32 ] ++ extraPackages32;
+      type = types.package;
+      default = pkgs.steam;
+      defaultText = literalExpression "pkgs.steam";
+      example = literalExpression ''
+        pkgs.steam-small.override {
+          extraEnv = {
+            MANGOHUD = true;
+            OBS_VKCAPTURE = true;
+            RADV_TEX_ANISO = 16;
+          };
+          extraLibraries = p: with p; [
+            atk
+          ];
         }
       '';
+      apply = steam: steam.override (prev: {
+        extraLibraries = pkgs: let
+          prevLibs = if prev ? extraLibraries then prev.extraLibraries pkgs else [ ];
+          additionalLibs = with config.hardware.opengl;
+            if pkgs.stdenv.hostPlatform.is64bit
+            then [ package ] ++ extraPackages
+            else [ package32 ] ++ extraPackages32;
+        in prevLibs ++ additionalLibs;
+      } // optionalAttrs (cfg.gamescopeSession.enable && gamescopeCfg.capSysNice)
+      {
+        buildFHSEnv = pkgs.buildFHSEnv.override {
+          # use the setuid wrapped bubblewrap
+          bubblewrap = "${config.security.wrapperDir}/..";
+        };
+      });
       description = lib.mdDoc ''
-        steam package to use.
+        The Steam package to use. Additional libraries are added from the system
+        configuration to ensure graphics work properly.
+
+        Use this option to customise the Steam package rather than adding your
+        custom Steam to {option}`environment.systemPackages` yourself.
       '';
     };
 
@@ -44,6 +81,31 @@ in {
         Open ports in the firewall for Source Dedicated Server.
       '';
     };
+
+    gamescopeSession = mkOption {
+      description = mdDoc "Run a GameScope driven Steam session from your display-manager";
+      default = {};
+      type = types.submodule {
+        options = {
+          enable = mkEnableOption (mdDoc "GameScope Session");
+          args = mkOption {
+            type = types.listOf types.string;
+            default = [ ];
+            description = mdDoc ''
+              Arguments to be passed to GameScope for the session.
+            '';
+          };
+
+          env = mkOption {
+            type = types.attrsOf types.string;
+            default = { };
+            description = mdDoc ''
+              Environmental variables to be passed to GameScope for the session.
+            '';
+          };
+        };
+      };
+    };
   };
 
   config = mkIf cfg.enable {
@@ -53,6 +115,19 @@ in {
       driSupport32Bit = true;
     };
 
+    security.wrappers = mkIf (cfg.gamescopeSession.enable && gamescopeCfg.capSysNice) {
+      # needed or steam fails
+      bwrap = {
+        owner = "root";
+        group = "root";
+        source = "${pkgs.bubblewrap}/bin/bwrap";
+        setuid = true;
+      };
+    };
+
+    programs.gamescope.enable = mkDefault cfg.gamescopeSession.enable;
+    services.xserver.displayManager.sessionPackages = mkIf cfg.gamescopeSession.enable [ gamescopeSessionFile ];
+
     # optionally enable 32bit pulseaudio support if pulseaudio is enabled
     hardware.pulseaudio.support32Bit = config.hardware.pulseaudio.enable;
 
@@ -61,7 +136,7 @@ in {
     environment.systemPackages = [
       cfg.package
       cfg.package.run
-    ];
+    ] ++ lib.optional cfg.gamescopeSession.enable steam-gamescope;
 
     networking.firewall = lib.mkMerge [
       (mkIf cfg.remotePlay.openFirewall {
diff --git a/nixos/modules/programs/streamdeck-ui.nix b/nixos/modules/programs/streamdeck-ui.nix
index 113d1d49e15..4c055029e39 100644
--- a/nixos/modules/programs/streamdeck-ui.nix
+++ b/nixos/modules/programs/streamdeck-ui.nix
@@ -15,7 +15,7 @@ in
       description = lib.mdDoc "Whether streamdeck-ui should be started automatically.";
     };
 
-    package = mkPackageOption pkgs "streamdeck-ui" {
+    package = mkPackageOptionMD pkgs "streamdeck-ui" {
       default = [ "streamdeck-ui" ];
     };
 
diff --git a/nixos/modules/programs/sway.nix b/nixos/modules/programs/sway.nix
index b0a766dd055..3b2e69bd37c 100644
--- a/nixos/modules/programs/sway.nix
+++ b/nixos/modules/programs/sway.nix
@@ -26,7 +26,7 @@ let
     };
   };
 
-  swayPackage = pkgs.sway.override {
+  defaultSwayPackage = pkgs.sway.override {
     extraSessionCommands = cfg.extraSessionCommands;
     extraOptions = cfg.extraOptions;
     withBaseWrapper = cfg.wrapperFeatures.base;
@@ -42,6 +42,19 @@ in {
       <https://github.com/swaywm/sway/wiki> and
       "man 5 sway" for more information'');
 
+    package = mkOption {
+      type = with types; nullOr package;
+      default = defaultSwayPackage;
+      defaultText = literalExpression "pkgs.sway";
+      description = lib.mdDoc ''
+        Sway package to use. Will override the options
+        'wrapperFeatures', 'extraSessionCommands', and 'extraOptions'.
+        Set to <code>null</code> to not add any Sway package to your
+        path. This should be done if you want to use the Home Manager Sway
+        module to install Sway.
+      '';
+    };
+
     wrapperFeatures = mkOption {
       type = wrapperOptions;
       default = { };
@@ -121,16 +134,17 @@ in {
       }
     ];
     environment = {
-      systemPackages = [ swayPackage ] ++ cfg.extraPackages;
+      systemPackages = optional (cfg.package != null) cfg.package ++ cfg.extraPackages;
       # Needed for the default wallpaper:
-      pathsToLink = [ "/share/backgrounds/sway" ];
+      pathsToLink = optionals (cfg.package != null) [ "/share/backgrounds/sway" ];
       etc = {
-        "sway/config".source = mkOptionDefault "${swayPackage}/etc/sway/config";
         "sway/config.d/nixos.conf".source = pkgs.writeText "nixos.conf" ''
           # Import the most important environment variables into the D-Bus and systemd
           # user environments (e.g. required for screen sharing and Pinentry prompts):
           exec dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY SWAYSOCK XDG_CURRENT_DESKTOP
         '';
+      } // optionalAttrs (cfg.package != null) {
+        "sway/config".source = mkOptionDefault "${cfg.package}/etc/sway/config";
       };
     };
     security.polkit.enable = true;
@@ -139,7 +153,7 @@ in {
     fonts.enableDefaultFonts = mkDefault true;
     programs.dconf.enable = mkDefault true;
     # To make a Sway session available if a display manager like SDDM is enabled:
-    services.xserver.displayManager.sessionPackages = [ swayPackage ];
+    services.xserver.displayManager.sessionPackages = optionals (cfg.package != null) [ cfg.package ];
     programs.xwayland.enable = mkDefault true;
     # For screen sharing (this option only has an effect with xdg.portal.enable):
     xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-wlr ];
diff --git a/nixos/modules/programs/tmux.nix b/nixos/modules/programs/tmux.nix
index 4fb9175fb8d..4f452f1d7f9 100644
--- a/nixos/modules/programs/tmux.nix
+++ b/nixos/modules/programs/tmux.nix
@@ -1,7 +1,7 @@
 { config, pkgs, lib, ... }:
 
 let
-  inherit (lib) mkOption mkIf types;
+  inherit (lib) mkOption mkIf types optionalString;
 
   cfg = config.programs.tmux;
 
@@ -17,17 +17,17 @@ let
     set  -g base-index      ${toString cfg.baseIndex}
     setw -g pane-base-index ${toString cfg.baseIndex}
 
-    ${if cfg.newSession then "new-session" else ""}
+    ${optionalString cfg.newSession "new-session"}
 
-    ${if cfg.reverseSplit then ''
+    ${optionalString cfg.reverseSplit ''
     bind v split-window -h
     bind s split-window -v
-    '' else ""}
+    ''}
 
     set -g status-keys ${cfg.keyMode}
     set -g mode-keys   ${cfg.keyMode}
 
-    ${if cfg.keyMode == "vi" && cfg.customPaneNavigationAndResize then ''
+    ${optionalString (cfg.keyMode == "vi" && cfg.customPaneNavigationAndResize) ''
     bind h select-pane -L
     bind j select-pane -D
     bind k select-pane -U
@@ -37,15 +37,15 @@ let
     bind -r J resize-pane -D ${toString cfg.resizeAmount}
     bind -r K resize-pane -U ${toString cfg.resizeAmount}
     bind -r L resize-pane -R ${toString cfg.resizeAmount}
-    '' else ""}
+    ''}
 
-    ${if (cfg.shortcut != defaultShortcut) then ''
+    ${optionalString (cfg.shortcut != defaultShortcut) ''
     # rebind main key: C-${cfg.shortcut}
     unbind C-${defaultShortcut}
     set -g prefix C-${cfg.shortcut}
     bind ${cfg.shortcut} send-prefix
     bind C-${cfg.shortcut} last-window
-    '' else ""}
+    ''}
 
     setw -g aggressive-resize ${boolToStr cfg.aggressiveResize}
     setw -g clock-mode-style  ${if cfg.clock24 then "24" else "12"}
@@ -160,7 +160,10 @@ in {
         default = defaultTerminal;
         example = "screen-256color";
         type = types.str;
-        description = lib.mdDoc "Set the $TERM variable.";
+        description = lib.mdDoc ''
+          Set the $TERM variable. Use tmux-direct if italics or 24bit true color
+          support is needed.
+        '';
       };
 
       secureSocket = mkOption {
diff --git a/nixos/modules/programs/tsm-client.nix b/nixos/modules/programs/tsm-client.nix
index 7adff7cd28c..41560544c2c 100644
--- a/nixos/modules/programs/tsm-client.nix
+++ b/nixos/modules/programs/tsm-client.nix
@@ -6,7 +6,7 @@ let
   inherit (lib.attrsets) attrNames filterAttrs hasAttr mapAttrs mapAttrsToList optionalAttrs;
   inherit (lib.modules) mkDefault mkIf;
   inherit (lib.options) literalExpression mkEnableOption mkOption;
-  inherit (lib.strings) concatStringsSep optionalString toLower;
+  inherit (lib.strings) concatLines optionalString toLower;
   inherit (lib.types) addCheck attrsOf lines nonEmptyStr nullOr package path port str strMatching submodule;
 
   # Checks if given list of strings contains unique
@@ -164,7 +164,7 @@ let
         mkLine = k: v: k + optionalString (v!="") "  ${v}";
         lines = mapAttrsToList mkLine attrset;
       in
-        concatStringsSep "\n" lines;
+        concatLines lines;
     config.stanza = ''
       server  ${config.name}
       ${config.text}
@@ -263,7 +263,7 @@ let
 
     ${optionalString (cfg.defaultServername!=null) "defaultserver  ${cfg.defaultServername}"}
 
-    ${concatStringsSep "\n" (mapAttrsToList (k: v: v.stanza) cfg.servers)}
+    ${concatLines (mapAttrsToList (k: v: v.stanza) cfg.servers)}
   '';
 
 in
diff --git a/nixos/modules/programs/waybar.nix b/nixos/modules/programs/waybar.nix
index 4697d0f7a62..2c49ae14081 100644
--- a/nixos/modules/programs/waybar.nix
+++ b/nixos/modules/programs/waybar.nix
@@ -2,17 +2,22 @@
 
 with lib;
 
+let
+  cfg = config.programs.waybar;
+in
 {
   options.programs.waybar = {
     enable = mkEnableOption (lib.mdDoc "waybar");
+    package = mkPackageOptionMD pkgs "waybar" { };
   };
 
-  config = mkIf config.programs.waybar.enable {
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
     systemd.user.services.waybar = {
       description = "Waybar as systemd service";
       wantedBy = [ "graphical-session.target" ];
       partOf = [ "graphical-session.target" ];
-      script = "${pkgs.waybar}/bin/waybar";
+      script = "${cfg.package}/bin/waybar";
     };
   };
 
diff --git a/nixos/modules/programs/wireshark.nix b/nixos/modules/programs/wireshark.nix
index 088c2bb7958..834b0ba3569 100644
--- a/nixos/modules/programs/wireshark.nix
+++ b/nixos/modules/programs/wireshark.nix
@@ -33,7 +33,7 @@ in {
 
     security.wrappers.dumpcap = {
       source = "${wireshark}/bin/dumpcap";
-      capabilities = "cap_net_raw+p";
+      capabilities = "cap_net_raw,cap_net_admin+eip";
       owner = "root";
       group = "wireshark";
       permissions = "u+rx,g+x";
diff --git a/nixos/modules/programs/xastir.nix b/nixos/modules/programs/xastir.nix
new file mode 100644
index 00000000000..6d5fc59aac5
--- /dev/null
+++ b/nixos/modules/programs/xastir.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.xastir;
+in {
+  meta.maintainers = with maintainers; [ melling ];
+
+  options.programs.xastir = {
+    enable = mkEnableOption (mdDoc "Xastir Graphical APRS client");
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = with pkgs; [ xastir ];
+    security.wrappers.xastir = {
+      source = "${pkgs.xastir}/bin/xastir";
+      capabilities = "cap_net_raw+p";
+      owner = "root";
+      group = "root";
+    };
+  };
+}
diff --git a/nixos/modules/programs/zsh/oh-my-zsh.md b/nixos/modules/programs/zsh/oh-my-zsh.md
new file mode 100644
index 00000000000..73d425244ce
--- /dev/null
+++ b/nixos/modules/programs/zsh/oh-my-zsh.md
@@ -0,0 +1,109 @@
+# Oh my ZSH {#module-programs-zsh-ohmyzsh}
+
+[`oh-my-zsh`](https://ohmyz.sh/) is a framework to manage your [ZSH](https://www.zsh.org/)
+configuration including completion scripts for several CLI tools or custom
+prompt themes.
+
+## Basic usage {#module-programs-oh-my-zsh-usage}
+
+The module uses the `oh-my-zsh` package with all available
+features. The initial setup using Nix expressions is fairly similar to the
+configuration format of `oh-my-zsh`.
+```
+{
+  programs.zsh.ohMyZsh = {
+    enable = true;
+    plugins = [ "git" "python" "man" ];
+    theme = "agnoster";
+  };
+}
+```
+For a detailed explanation of these arguments please refer to the
+[`oh-my-zsh` docs](https://github.com/robbyrussell/oh-my-zsh/wiki).
+
+The expression generates the needed configuration and writes it into your
+`/etc/zshrc`.
+
+## Custom additions {#module-programs-oh-my-zsh-additions}
+
+Sometimes third-party or custom scripts such as a modified theme may be
+needed. `oh-my-zsh` provides the
+[`ZSH_CUSTOM`](https://github.com/robbyrussell/oh-my-zsh/wiki/Customization#overriding-internals)
+environment variable for this which points to a directory with additional
+scripts.
+
+The module can do this as well:
+```
+{
+  programs.zsh.ohMyZsh.custom = "~/path/to/custom/scripts";
+}
+```
+
+## Custom environments {#module-programs-oh-my-zsh-environments}
+
+There are several extensions for `oh-my-zsh` packaged in
+`nixpkgs`. One of them is
+[nix-zsh-completions](https://github.com/spwhitt/nix-zsh-completions)
+which bundles completion scripts and a plugin for `oh-my-zsh`.
+
+Rather than using a single mutable path for `ZSH_CUSTOM`,
+it's also possible to generate this path from a list of Nix packages:
+```
+{ pkgs, ... }:
+{
+  programs.zsh.ohMyZsh.customPkgs = [
+    pkgs.nix-zsh-completions
+    # and even more...
+  ];
+}
+```
+Internally a single store path will be created using
+`buildEnv`. Please refer to the docs of
+[`buildEnv`](https://nixos.org/nixpkgs/manual/#sec-building-environment)
+for further reference.
+
+*Please keep in mind that this is not compatible with
+`programs.zsh.ohMyZsh.custom` as it requires an immutable
+store path while `custom` shall remain mutable! An
+evaluation failure will be thrown if both `custom` and
+`customPkgs` are set.*
+
+## Package your own customizations {#module-programs-oh-my-zsh-packaging-customizations}
+
+If third-party customizations (e.g. new themes) are supposed to be added to
+`oh-my-zsh` there are several pitfalls to keep in mind:
+
+  - To comply with the default structure of `ZSH` the entire
+    output needs to be written to `$out/share/zsh.`
+
+  - Completion scripts are supposed to be stored at
+    `$out/share/zsh/site-functions`. This directory is part of the
+    [`fpath`](http://zsh.sourceforge.net/Doc/Release/Functions.html)
+    and the package should be compatible with pure `ZSH`
+    setups. The module will automatically link the contents of
+    `site-functions` to completions directory in the proper
+    store path.
+
+  - The `plugins` directory needs the structure
+    `pluginname/pluginname.plugin.zsh` as structured in the
+    [upstream repo.](https://github.com/robbyrussell/oh-my-zsh/tree/91b771914bc7c43dd7c7a43b586c5de2c225ceb7/plugins)
+
+A derivation for `oh-my-zsh` may look like this:
+```
+{ stdenv, fetchFromGitHub }:
+
+stdenv.mkDerivation rec {
+  name = "exemplary-zsh-customization-${version}";
+  version = "1.0.0";
+  src = fetchFromGitHub {
+    # path to the upstream repository
+  };
+
+  dontBuild = true;
+  installPhase = ''
+    mkdir -p $out/share/zsh/site-functions
+    cp {themes,plugins} $out/share/zsh
+    cp completions $out/share/zsh/site-functions
+  '';
+}
+```
diff --git a/nixos/modules/programs/zsh/oh-my-zsh.nix b/nixos/modules/programs/zsh/oh-my-zsh.nix
index 41ea31b0f12..83eee1c88b3 100644
--- a/nixos/modules/programs/zsh/oh-my-zsh.nix
+++ b/nixos/modules/programs/zsh/oh-my-zsh.nix
@@ -142,5 +142,5 @@ in
 
     };
 
-    meta.doc = ./oh-my-zsh.xml;
+    meta.doc = ./oh-my-zsh.md;
   }
diff --git a/nixos/modules/programs/zsh/oh-my-zsh.xml b/nixos/modules/programs/zsh/oh-my-zsh.xml
deleted file mode 100644
index 14a7228ad9b..00000000000
--- a/nixos/modules/programs/zsh/oh-my-zsh.xml
+++ /dev/null
@@ -1,155 +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-programs-zsh-ohmyzsh">
- <title>Oh my ZSH</title>
- <para>
-  <literal><link xlink:href="https://ohmyz.sh/">oh-my-zsh</link></literal> is a
-  framework to manage your <link xlink:href="https://www.zsh.org/">ZSH</link>
-  configuration including completion scripts for several CLI tools or custom
-  prompt themes.
- </para>
- <section xml:id="module-programs-oh-my-zsh-usage">
-  <title>Basic usage</title>
-
-  <para>
-   The module uses the <literal>oh-my-zsh</literal> package with all available
-   features. The initial setup using Nix expressions is fairly similar to the
-   configuration format of <literal>oh-my-zsh</literal>.
-<programlisting>
-{
-  programs.zsh.ohMyZsh = {
-    enable = true;
-    plugins = [ "git" "python" "man" ];
-    theme = "agnoster";
-  };
-}
-</programlisting>
-   For a detailed explanation of these arguments please refer to the
-   <link xlink:href="https://github.com/robbyrussell/oh-my-zsh/wiki"><literal>oh-my-zsh</literal>
-   docs</link>.
-  </para>
-
-  <para>
-   The expression generates the needed configuration and writes it into your
-   <literal>/etc/zshrc</literal>.
-  </para>
- </section>
- <section xml:id="module-programs-oh-my-zsh-additions">
-  <title>Custom additions</title>
-
-  <para>
-   Sometimes third-party or custom scripts such as a modified theme may be
-   needed. <literal>oh-my-zsh</literal> provides the
-   <link xlink:href="https://github.com/robbyrussell/oh-my-zsh/wiki/Customization#overriding-internals"><literal>ZSH_CUSTOM</literal></link>
-   environment variable for this which points to a directory with additional
-   scripts.
-  </para>
-
-  <para>
-   The module can do this as well:
-<programlisting>
-{
-  programs.zsh.ohMyZsh.custom = "~/path/to/custom/scripts";
-}
-</programlisting>
-  </para>
- </section>
- <section xml:id="module-programs-oh-my-zsh-environments">
-  <title>Custom environments</title>
-
-  <para>
-   There are several extensions for <literal>oh-my-zsh</literal> packaged in
-   <literal>nixpkgs</literal>. One of them is
-   <link xlink:href="https://github.com/spwhitt/nix-zsh-completions">nix-zsh-completions</link>
-   which bundles completion scripts and a plugin for
-   <literal>oh-my-zsh</literal>.
-  </para>
-
-  <para>
-   Rather than using a single mutable path for <literal>ZSH_CUSTOM</literal>,
-   it's also possible to generate this path from a list of Nix packages:
-<programlisting>
-{ pkgs, ... }:
-{
-  programs.zsh.ohMyZsh.customPkgs = [
-    pkgs.nix-zsh-completions
-    # and even more...
-  ];
-}
-</programlisting>
-   Internally a single store path will be created using
-   <literal>buildEnv</literal>. Please refer to the docs of
-   <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-building-environment"><literal>buildEnv</literal></link>
-   for further reference.
-  </para>
-
-  <para>
-   <emphasis>Please keep in mind that this is not compatible with
-   <literal>programs.zsh.ohMyZsh.custom</literal> as it requires an immutable
-   store path while <literal>custom</literal> shall remain mutable! An
-   evaluation failure will be thrown if both <literal>custom</literal> and
-   <literal>customPkgs</literal> are set.</emphasis>
-  </para>
- </section>
- <section xml:id="module-programs-oh-my-zsh-packaging-customizations">
-  <title>Package your own customizations</title>
-
-  <para>
-   If third-party customizations (e.g. new themes) are supposed to be added to
-   <literal>oh-my-zsh</literal> there are several pitfalls to keep in mind:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     To comply with the default structure of <literal>ZSH</literal> the entire
-     output needs to be written to <literal>$out/share/zsh.</literal>
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Completion scripts are supposed to be stored at
-     <literal>$out/share/zsh/site-functions</literal>. This directory is part
-     of the
-     <literal><link xlink:href="http://zsh.sourceforge.net/Doc/Release/Functions.html">fpath</link></literal>
-     and the package should be compatible with pure <literal>ZSH</literal>
-     setups. The module will automatically link the contents of
-     <literal>site-functions</literal> to completions directory in the proper
-     store path.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     The <literal>plugins</literal> directory needs the structure
-     <literal>pluginname/pluginname.plugin.zsh</literal> as structured in the
-     <link xlink:href="https://github.com/robbyrussell/oh-my-zsh/tree/91b771914bc7c43dd7c7a43b586c5de2c225ceb7/plugins">upstream
-     repo.</link>
-    </para>
-   </listitem>
-  </itemizedlist>
-
-  <para>
-   A derivation for <literal>oh-my-zsh</literal> may look like this:
-<programlisting>
-{ stdenv, fetchFromGitHub }:
-
-stdenv.mkDerivation rec {
-  name = "exemplary-zsh-customization-${version}";
-  version = "1.0.0";
-  src = fetchFromGitHub {
-    # path to the upstream repository
-  };
-
-  dontBuild = true;
-  installPhase = ''
-    mkdir -p $out/share/zsh/site-functions
-    cp {themes,plugins} $out/share/zsh
-    cp completions $out/share/zsh/site-functions
-  '';
-}
-</programlisting>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
index bc110134928..cec4be1cb01 100644
--- a/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
+++ b/nixos/modules/programs/zsh/zsh-syntax-highlighting.nix
@@ -26,6 +26,7 @@ in
           "brackets"
           "pattern"
           "cursor"
+          "regexp"
           "root"
           "line"
         ]));
diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix
index 0b152e54cf9..6bb21cb3ef6 100644
--- a/nixos/modules/programs/zsh/zsh.nix
+++ b/nixos/modules/programs/zsh/zsh.nix
@@ -236,6 +236,9 @@ in
           setopt ${concatStringsSep " " cfg.setOptions}
         ''}
 
+        # Alternative method of determining short and full hostname.
+        HOST=${config.networking.fqdnOrHostName}
+
         # Setup command line history.
         # Don't export these, otherwise other shells (bash) will try to use same HISTFILE.
         SAVEHIST=${toString cfg.histSize}
diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix
index aef42d0f4db..45a27029dff 100644
--- a/nixos/modules/rename.nix
+++ b/nixos/modules/rename.nix
@@ -14,7 +14,7 @@ with lib;
 
     # This alias module can't be where _module.check is defined because it would
     # be added to submodules as well there
-    (mkAliasOptionModule [ "environment" "checkConfigurationOptions" ] [ "_module" "check" ])
+    (mkAliasOptionModuleMD [ "environment" "checkConfigurationOptions" ] [ "_module" "check" ])
 
     # Completely removed modules
     (mkRemovedOptionModule [ "environment" "blcr" "enable" ] "The BLCR module has been removed")
@@ -36,6 +36,7 @@ with lib;
     '')
     (mkRemovedOptionModule [ "networking" "vpnc" ] "Use environment.etc.\"vpnc/service.conf\" instead.")
     (mkRemovedOptionModule [ "networking" "wicd" ] "The corresponding package was removed from nixpkgs.")
+    (mkRemovedOptionModule [ "programs" "gnome-documents" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "programs" "tilp2" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "programs" "way-cooler" ] ("way-cooler is abandoned by its author: " +
       "https://way-cooler.org/blog/2020/01/09/way-cooler-post-mortem.html"))
@@ -43,13 +44,13 @@ with lib;
         The hidepid module was removed, since the underlying machinery
         is broken when using cgroups-v2.
     '')
+    (mkRemovedOptionModule [ "services" "baget" "enable" ] "The baget module was removed due to the upstream package being unmaintained.")
     (mkRemovedOptionModule [ "services" "beegfs" ] "The BeeGFS module has been removed")
     (mkRemovedOptionModule [ "services" "beegfsEnable" ] "The BeeGFS module has been removed")
     (mkRemovedOptionModule [ "services" "cgmanager" "enable"] "cgmanager was deprecated by lxc and therefore removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "chronos" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "couchpotato" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "dd-agent" ] "dd-agent was removed from nixpkgs in favor of the newer datadog-agent.")
-    (mkRemovedOptionModule [ "services" "deepin" ] "The corresponding packages were removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "dnscrypt-proxy" ] "Use services.dnscrypt-proxy2 instead")
     (mkRemovedOptionModule [ "services" "firefox" "syncserver" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "flashpolicyd" ] "The flashpolicyd module has been removed. Adobe Flash Player is deprecated.")
@@ -57,6 +58,7 @@ with lib;
     (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" "ihatemoney" ] "The ihatemoney module has been removed for lack of downstream maintainer")
     (mkRemovedOptionModule [ "services" "kippo" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "mailpile" ] "The corresponding package was removed from nixpkgs.")
     (mkRemovedOptionModule [ "services" "marathon" ] "The corresponding package was removed from nixpkgs.")
@@ -105,6 +107,9 @@ with lib;
     (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.")
+    (mkRemovedOptionModule [ "services" "rtsp-simple-server" ] "Package has been completely rebranded by upstream as mediamtx, and thus the service and the package were renamed in NixOS as well.")
+
+    (mkRemovedOptionModule [ "i18n" "inputMethod" "fcitx" ] "The fcitx module has been removed. Plesae use fcitx5 instead")
 
     # Do NOT add any option renames here, see top of the file
   ];
diff --git a/nixos/modules/security/acme/default.md b/nixos/modules/security/acme/default.md
new file mode 100644
index 00000000000..8ff97b55f68
--- /dev/null
+++ b/nixos/modules/security/acme/default.md
@@ -0,0 +1,354 @@
+# SSL/TLS Certificates with ACME {#module-security-acme}
+
+NixOS supports automatic domain validation & certificate retrieval and
+renewal using the ACME protocol. Any provider can be used, but by default
+NixOS uses Let's Encrypt. The alternative ACME client
+[lego](https://go-acme.github.io/lego/) is used under
+the hood.
+
+Automatic cert validation and configuration for Apache and Nginx virtual
+hosts is included in NixOS, however if you would like to generate a wildcard
+cert or you are not using a web server you will have to configure DNS
+based validation.
+
+## Prerequisites {#module-security-acme-prerequisites}
+
+To use the ACME module, you must accept the provider's terms of service
+by setting [](#opt-security.acme.acceptTerms)
+to `true`. The Let's Encrypt ToS can be found
+[here](https://letsencrypt.org/repository/).
+
+You must also set an email address to be used when creating accounts with
+Let's Encrypt. You can set this for all certs with
+[](#opt-security.acme.defaults.email)
+and/or on a per-cert basis with
+[](#opt-security.acme.certs._name_.email).
+This address is only used for registration and renewal reminders,
+and cannot be used to administer the certificates in any way.
+
+Alternatively, you can use a different ACME server by changing the
+[](#opt-security.acme.defaults.server) option
+to a provider of your choosing, or just change the server for one cert with
+[](#opt-security.acme.certs._name_.server).
+
+You will need an HTTP server or DNS server for verification. For HTTP,
+the server must have a webroot defined that can serve
+{file}`.well-known/acme-challenge`. This directory must be
+writeable by the user that will run the ACME client. For DNS, you must
+set up credentials with your provider/server for use with lego.
+
+## Using ACME certificates in Nginx {#module-security-acme-nginx}
+
+NixOS supports fetching ACME certificates for you by setting
+`enableACME = true;` in a virtualHost config. We first create self-signed
+placeholder certificates in place of the real ACME certs. The placeholder
+certs are overwritten when the ACME certs arrive. For
+`foo.example.com` the config would look like this:
+
+```
+security.acme.acceptTerms = true;
+security.acme.defaults.email = "admin+acme@example.com";
+services.nginx = {
+  enable = true;
+  virtualHosts = {
+    "foo.example.com" = {
+      forceSSL = true;
+      enableACME = true;
+      # All serverAliases will be added as extra domain names on the certificate.
+      serverAliases = [ "bar.example.com" ];
+      locations."/" = {
+        root = "/var/www";
+      };
+    };
+
+    # We can also add a different vhost and reuse the same certificate
+    # but we have to append extraDomainNames manually beforehand:
+    # security.acme.certs."foo.example.com".extraDomainNames = [ "baz.example.com" ];
+    "baz.example.com" = {
+      forceSSL = true;
+      useACMEHost = "foo.example.com";
+      locations."/" = {
+        root = "/var/www";
+      };
+    };
+  };
+}
+```
+
+## Using ACME certificates in Apache/httpd {#module-security-acme-httpd}
+
+Using ACME certificates with Apache virtual hosts is identical
+to using them with Nginx. The attribute names are all the same, just replace
+"nginx" with "httpd" where appropriate.
+
+## Manual configuration of HTTP-01 validation {#module-security-acme-configuring}
+
+First off you will need to set up a virtual host to serve the challenges.
+This example uses a vhost called `certs.example.com`, with
+the intent that you will generate certs for all your vhosts and redirect
+everyone to HTTPS.
+
+```
+security.acme.acceptTerms = true;
+security.acme.defaults.email = "admin+acme@example.com";
+
+# /var/lib/acme/.challenges must be writable by the ACME user
+# and readable by the Nginx user. The easiest way to achieve
+# this is to add the Nginx user to the ACME group.
+users.users.nginx.extraGroups = [ "acme" ];
+
+services.nginx = {
+  enable = true;
+  virtualHosts = {
+    "acmechallenge.example.com" = {
+      # Catchall vhost, will redirect users to HTTPS for all vhosts
+      serverAliases = [ "*.example.com" ];
+      locations."/.well-known/acme-challenge" = {
+        root = "/var/lib/acme/.challenges";
+      };
+      locations."/" = {
+        return = "301 https://$host$request_uri";
+      };
+    };
+  };
+}
+# Alternative config for Apache
+users.users.wwwrun.extraGroups = [ "acme" ];
+services.httpd = {
+  enable = true;
+  virtualHosts = {
+    "acmechallenge.example.com" = {
+      # Catchall vhost, will redirect users to HTTPS for all vhosts
+      serverAliases = [ "*.example.com" ];
+      # /var/lib/acme/.challenges must be writable by the ACME user and readable by the Apache user.
+      # By default, this is the case.
+      documentRoot = "/var/lib/acme/.challenges";
+      extraConfig = ''
+        RewriteEngine On
+        RewriteCond %{HTTPS} off
+        RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge [NC]
+        RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301]
+      '';
+    };
+  };
+}
+```
+
+Now you need to configure ACME to generate a certificate.
+
+```
+security.acme.certs."foo.example.com" = {
+  webroot = "/var/lib/acme/.challenges";
+  email = "foo@example.com";
+  # Ensure that the web server you use can read the generated certs
+  # Take a look at the group option for the web server you choose.
+  group = "nginx";
+  # Since we have a wildcard vhost to handle port 80,
+  # we can generate certs for anything!
+  # Just make sure your DNS resolves them.
+  extraDomainNames = [ "mail.example.com" ];
+};
+```
+
+The private key {file}`key.pem` and certificate
+{file}`fullchain.pem` will be put into
+{file}`/var/lib/acme/foo.example.com`.
+
+Refer to [](#ch-options) for all available configuration
+options for the [security.acme](#opt-security.acme.certs)
+module.
+
+## Configuring ACME for DNS validation {#module-security-acme-config-dns}
+
+This is useful if you want to generate a wildcard certificate, since
+ACME servers will only hand out wildcard certs over DNS validation.
+There are a number of supported DNS providers and servers you can utilise,
+see the [lego docs](https://go-acme.github.io/lego/dns/)
+for provider/server specific configuration values. For the sake of these
+docs, we will provide a fully self-hosted example using bind.
+
+```
+services.bind = {
+  enable = true;
+  extraConfig = ''
+    include "/var/lib/secrets/dnskeys.conf";
+  '';
+  zones = [
+    rec {
+      name = "example.com";
+      file = "/var/db/bind/${name}";
+      master = true;
+      extraConfig = "allow-update { key rfc2136key.example.com.; };";
+    }
+  ];
+}
+
+# Now we can configure ACME
+security.acme.acceptTerms = true;
+security.acme.defaults.email = "admin+acme@example.com";
+security.acme.certs."example.com" = {
+  domain = "*.example.com";
+  dnsProvider = "rfc2136";
+  credentialsFile = "/var/lib/secrets/certs.secret";
+  # We don't need to wait for propagation since this is a local DNS server
+  dnsPropagationCheck = false;
+};
+```
+
+The {file}`dnskeys.conf` and {file}`certs.secret`
+must be kept secure and thus you should not keep their contents in your
+Nix config. Instead, generate them one time with a systemd service:
+
+```
+systemd.services.dns-rfc2136-conf = {
+  requiredBy = ["acme-example.com.service" "bind.service"];
+  before = ["acme-example.com.service" "bind.service"];
+  unitConfig = {
+    ConditionPathExists = "!/var/lib/secrets/dnskeys.conf";
+  };
+  serviceConfig = {
+    Type = "oneshot";
+    UMask = 0077;
+  };
+  path = [ pkgs.bind ];
+  script = ''
+    mkdir -p /var/lib/secrets
+    chmod 755 /var/lib/secrets
+    tsig-keygen rfc2136key.example.com > /var/lib/secrets/dnskeys.conf
+    chown named:root /var/lib/secrets/dnskeys.conf
+    chmod 400 /var/lib/secrets/dnskeys.conf
+
+    # extract secret value from the dnskeys.conf
+    while read x y; do if [ "$x" = "secret" ]; then secret="''${y:1:''${#y}-3}"; fi; done < /var/lib/secrets/dnskeys.conf
+
+    cat > /var/lib/secrets/certs.secret << EOF
+    RFC2136_NAMESERVER='127.0.0.1:53'
+    RFC2136_TSIG_ALGORITHM='hmac-sha256.'
+    RFC2136_TSIG_KEY='rfc2136key.example.com'
+    RFC2136_TSIG_SECRET='$secret'
+    EOF
+    chmod 400 /var/lib/secrets/certs.secret
+  '';
+};
+```
+
+Now you're all set to generate certs! You should monitor the first invocation
+by running `systemctl start acme-example.com.service &
+journalctl -fu acme-example.com.service` and watching its log output.
+
+## Using DNS validation with web server virtual hosts {#module-security-acme-config-dns-with-vhosts}
+
+It is possible to use DNS-01 validation with all certificates,
+including those automatically configured via the Nginx/Apache
+[`enableACME`](#opt-services.nginx.virtualHosts._name_.enableACME)
+option. This configuration pattern is fully
+supported and part of the module's test suite for Nginx + Apache.
+
+You must follow the guide above on configuring DNS-01 validation
+first, however instead of setting the options for one certificate
+(e.g. [](#opt-security.acme.certs._name_.dnsProvider))
+you will set them as defaults
+(e.g. [](#opt-security.acme.defaults.dnsProvider)).
+
+```
+# Configure ACME appropriately
+security.acme.acceptTerms = true;
+security.acme.defaults.email = "admin+acme@example.com";
+security.acme.defaults = {
+  dnsProvider = "rfc2136";
+  credentialsFile = "/var/lib/secrets/certs.secret";
+  # We don't need to wait for propagation since this is a local DNS server
+  dnsPropagationCheck = false;
+};
+
+# For each virtual host you would like to use DNS-01 validation with,
+# set acmeRoot = null
+services.nginx = {
+  enable = true;
+  virtualHosts = {
+    "foo.example.com" = {
+      enableACME = true;
+      acmeRoot = null;
+    };
+  };
+}
+```
+
+And that's it! Next time your configuration is rebuilt, or when
+you add a new virtualHost, it will be DNS-01 validated.
+
+## Using ACME with services demanding root owned certificates {#module-security-acme-root-owned}
+
+Some services refuse to start if the configured certificate files
+are not owned by root. PostgreSQL and OpenSMTPD are examples of these.
+There is no way to change the user the ACME module uses (it will always be
+`acme`), however you can use systemd's
+`LoadCredential` feature to resolve this elegantly.
+Below is an example configuration for OpenSMTPD, but this pattern
+can be applied to any service.
+
+```
+# Configure ACME however you like (DNS or HTTP validation), adding
+# the following configuration for the relevant certificate.
+# Note: You cannot use `systemctl reload` here as that would mean
+# the LoadCredential configuration below would be skipped and
+# the service would continue to use old certificates.
+security.acme.certs."mail.example.com".postRun = ''
+  systemctl restart opensmtpd
+'';
+
+# Now you must augment OpenSMTPD's systemd service to load
+# the certificate files.
+systemd.services.opensmtpd.requires = ["acme-finished-mail.example.com.target"];
+systemd.services.opensmtpd.serviceConfig.LoadCredential = let
+  certDir = config.security.acme.certs."mail.example.com".directory;
+in [
+  "cert.pem:${certDir}/cert.pem"
+  "key.pem:${certDir}/key.pem"
+];
+
+# Finally, configure OpenSMTPD to use these certs.
+services.opensmtpd = let
+  credsDir = "/run/credentials/opensmtpd.service";
+in {
+  enable = true;
+  setSendmail = false;
+  serverConfiguration = ''
+    pki mail.example.com cert "${credsDir}/cert.pem"
+    pki mail.example.com key "${credsDir}/key.pem"
+    listen on localhost tls pki mail.example.com
+    action act1 relay host smtp://127.0.0.1:10027
+    match for local action act1
+  '';
+};
+```
+
+## Regenerating certificates {#module-security-acme-regenerate}
+
+Should you need to regenerate a particular certificate in a hurry, such
+as when a vulnerability is found in Let's Encrypt, there is now a convenient
+mechanism for doing so. Running
+`systemctl clean --what=state acme-example.com.service`
+will remove all certificate files and the account data for the given domain,
+allowing you to then `systemctl start acme-example.com.service`
+to generate fresh ones.
+
+## Fixing JWS Verification error {#module-security-acme-fix-jws}
+
+It is possible that your account credentials file may become corrupt and need
+to be regenerated. In this scenario lego will produce the error `JWS verification error`.
+The solution is to simply delete the associated accounts file and
+re-run the affected service(s).
+
+```
+# Find the accounts folder for the certificate
+systemctl cat acme-example.com.service | grep -Po 'accounts/[^:]*'
+export accountdir="$(!!)"
+# Move this folder to some place else
+mv /var/lib/acme/.lego/$accountdir{,.bak}
+# Recreate the folder using systemd-tmpfiles
+systemd-tmpfiles --create
+# Get a new account and reissue certificates
+# Note: Do this for all certs that share the same account email address
+systemctl start acme-example.com.service
+```
diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix
index a380bb5484a..ea94b54312c 100644
--- a/nixos/modules/security/acme/default.nix
+++ b/nixos/modules/security/acme/default.nix
@@ -487,7 +487,7 @@ let
       };
 
       email = mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         inherit (defaultAndText "email" null) default defaultText;
         description = lib.mdDoc ''
           Email address for account creation and correspondence from the CA.
@@ -555,7 +555,7 @@ let
       };
 
       credentialsFile = mkOption {
-        type = types.path;
+        type = types.nullOr types.path;
         inherit (defaultAndText "credentialsFile" null) default defaultText;
         description = lib.mdDoc ''
           Path to an EnvironmentFile for the cert's service containing any required and
@@ -727,7 +727,7 @@ in {
           Default values inheritable by all configured certs. You can
           use this to define options shared by all your certs. These defaults
           can also be ignored on a per-cert basis using the
-          `security.acme.certs.''${cert}.inheritDefaults' option.
+          {option}`security.acme.certs.''${cert}.inheritDefaults` option.
         '';
       };
 
@@ -781,11 +781,11 @@ in {
 
       # FIXME Most of these custom warnings and filters for security.acme.certs.* are required
       # because using mkRemovedOptionModule/mkChangedOptionModule with attrsets isn't possible.
-      warnings = filter (w: w != "") (mapAttrsToList (cert: data: if data.extraDomains != "_mkMergedOptionModule" then ''
+      warnings = filter (w: w != "") (mapAttrsToList (cert: data: optionalString (data.extraDomains != "_mkMergedOptionModule") ''
         The option definition `security.acme.certs.${cert}.extraDomains` has changed
         to `security.acme.certs.${cert}.extraDomainNames` and is now a list of strings.
         Setting a custom webroot for extra domains is not possible, instead use separate certs.
-      '' else "") cfg.certs);
+      '') cfg.certs);
 
       assertions = let
         certs = attrValues cfg.certs;
@@ -916,6 +916,6 @@ in {
 
   meta = {
     maintainers = lib.teams.acme.members;
-    doc = ./doc.xml;
+    doc = ./default.md;
   };
 }
diff --git a/nixos/modules/security/acme/doc.xml b/nixos/modules/security/acme/doc.xml
deleted file mode 100644
index 1439594a5ac..00000000000
--- a/nixos/modules/security/acme/doc.xml
+++ /dev/null
@@ -1,414 +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-security-acme">
- <title>SSL/TLS Certificates with ACME</title>
- <para>
-  NixOS supports automatic domain validation &amp; certificate retrieval and
-  renewal using the ACME protocol. Any provider can be used, but by default
-  NixOS uses Let's Encrypt. The alternative ACME client
-  <link xlink:href="https://go-acme.github.io/lego/">lego</link> is used under
-  the hood.
- </para>
- <para>
-  Automatic cert validation and configuration for Apache and Nginx virtual
-  hosts is included in NixOS, however if you would like to generate a wildcard
-  cert or you are not using a web server you will have to configure DNS
-  based validation.
- </para>
- <section xml:id="module-security-acme-prerequisites">
-  <title>Prerequisites</title>
-
-  <para>
-   To use the ACME module, you must accept the provider's terms of service
-   by setting <literal><xref linkend="opt-security.acme.acceptTerms" /></literal>
-   to <literal>true</literal>. The Let's Encrypt ToS can be found
-   <link xlink:href="https://letsencrypt.org/repository/">here</link>.
-  </para>
-
-  <para>
-   You must also set an email address to be used when creating accounts with
-   Let's Encrypt. You can set this for all certs with
-   <literal><xref linkend="opt-security.acme.defaults.email" /></literal>
-   and/or on a per-cert basis with
-   <literal><xref linkend="opt-security.acme.certs._name_.email" /></literal>.
-   This address is only used for registration and renewal reminders,
-   and cannot be used to administer the certificates in any way.
-  </para>
-
-  <para>
-   Alternatively, you can use a different ACME server by changing the
-   <literal><xref linkend="opt-security.acme.defaults.server" /></literal> option
-   to a provider of your choosing, or just change the server for one cert with
-   <literal><xref linkend="opt-security.acme.certs._name_.server" /></literal>.
-  </para>
-
-  <para>
-   You will need an HTTP server or DNS server for verification. For HTTP,
-   the server must have a webroot defined that can serve
-   <filename>.well-known/acme-challenge</filename>. This directory must be
-   writeable by the user that will run the ACME client. For DNS, you must
-   set up credentials with your provider/server for use with lego.
-  </para>
- </section>
- <section xml:id="module-security-acme-nginx">
-  <title>Using ACME certificates in Nginx</title>
-
-  <para>
-   NixOS supports fetching ACME certificates for you by setting
-   <literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link>
-   = true;</literal> in a virtualHost config. We first create self-signed
-   placeholder certificates in place of the real ACME certs. The placeholder
-   certs are overwritten when the ACME certs arrive. For
-   <literal>foo.example.com</literal> the config would look like this:
-  </para>
-
-<programlisting>
-<xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
-services.nginx = {
-  <link linkend="opt-services.nginx.enable">enable</link> = true;
-  <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
-    "foo.example.com" = {
-      <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
-      <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
-      # All serverAliases will be added as <link linkend="opt-security.acme.certs._name_.extraDomainNames">extra domain names</link> on the certificate.
-      <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "bar.example.com" ];
-      locations."/" = {
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www";
-      };
-    };
-
-    # We can also add a different vhost and reuse the same certificate
-    # but we have to append extraDomainNames manually beforehand:
-    # <link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."foo.example.com".extraDomainNames</link> = [ "baz.example.com" ];
-    "baz.example.com" = {
-      <link linkend="opt-services.nginx.virtualHosts._name_.forceSSL">forceSSL</link> = true;
-      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">useACMEHost</link> = "foo.example.com";
-      locations."/" = {
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/www";
-      };
-    };
-  };
-}
-</programlisting>
- </section>
- <section xml:id="module-security-acme-httpd">
-  <title>Using ACME certificates in Apache/httpd</title>
-
-  <para>
-   Using ACME certificates with Apache virtual hosts is identical
-   to using them with Nginx. The attribute names are all the same, just replace
-   "nginx" with "httpd" where appropriate.
-  </para>
- </section>
- <section xml:id="module-security-acme-configuring">
-  <title>Manual configuration of HTTP-01 validation</title>
-
-  <para>
-   First off you will need to set up a virtual host to serve the challenges.
-   This example uses a vhost called <literal>certs.example.com</literal>, with
-   the intent that you will generate certs for all your vhosts and redirect
-   everyone to HTTPS.
-  </para>
-
-<programlisting>
-<xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
-
-# /var/lib/acme/.challenges must be writable by the ACME user
-# and readable by the Nginx user. The easiest way to achieve
-# this is to add the Nginx user to the ACME group.
-<link linkend="opt-users.users._name_.extraGroups">users.users.nginx.extraGroups</link> = [ "acme" ];
-
-services.nginx = {
-  <link linkend="opt-services.nginx.enable">enable</link> = true;
-  <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
-    "acmechallenge.example.com" = {
-      # Catchall vhost, will redirect users to HTTPS for all vhosts
-      <link linkend="opt-services.nginx.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ];
-      locations."/.well-known/acme-challenge" = {
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.root">root</link> = "/var/lib/acme/.challenges";
-      };
-      locations."/" = {
-        <link linkend="opt-services.nginx.virtualHosts._name_.locations._name_.return">return</link> = "301 https://$host$request_uri";
-      };
-    };
-  };
-}
-# Alternative config for Apache
-<link linkend="opt-users.users._name_.extraGroups">users.users.wwwrun.extraGroups</link> = [ "acme" ];
-services.httpd = {
-  <link linkend="opt-services.httpd.enable">enable = true;</link>
-  <link linkend="opt-services.httpd.virtualHosts">virtualHosts</link> = {
-    "acmechallenge.example.com" = {
-      # Catchall vhost, will redirect users to HTTPS for all vhosts
-      <link linkend="opt-services.httpd.virtualHosts._name_.serverAliases">serverAliases</link> = [ "*.example.com" ];
-      # /var/lib/acme/.challenges must be writable by the ACME user and readable by the Apache user.
-      # By default, this is the case.
-      <link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = "/var/lib/acme/.challenges";
-      <link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = ''
-        RewriteEngine On
-        RewriteCond %{HTTPS} off
-        RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge [NC]
-        RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R=301]
-      '';
-    };
-  };
-}
-</programlisting>
-
-  <para>
-   Now you need to configure ACME to generate a certificate.
-  </para>
-
-<programlisting>
-<xref linkend="opt-security.acme.certs"/>."foo.example.com" = {
-  <link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/lib/acme/.challenges";
-  <link linkend="opt-security.acme.certs._name_.email">email</link> = "foo@example.com";
-  # Ensure that the web server you use can read the generated certs
-  # Take a look at the <link linkend="opt-services.nginx.group">group</link> option for the web server you choose.
-  <link linkend="opt-security.acme.certs._name_.group">group</link> = "nginx";
-  # Since we have a wildcard vhost to handle port 80,
-  # we can generate certs for anything!
-  # Just make sure your DNS resolves them.
-  <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "mail.example.com" ];
-};
-</programlisting>
-
-  <para>
-   The private key <filename>key.pem</filename> and certificate
-   <filename>fullchain.pem</filename> will be put into
-   <filename>/var/lib/acme/foo.example.com</filename>.
-  </para>
-
-  <para>
-   Refer to <xref linkend="ch-options" /> for all available configuration
-   options for the <link linkend="opt-security.acme.certs">security.acme</link>
-   module.
-  </para>
- </section>
- <section xml:id="module-security-acme-config-dns">
-  <title>Configuring ACME for DNS validation</title>
-
-  <para>
-   This is useful if you want to generate a wildcard certificate, since
-   ACME servers will only hand out wildcard certs over DNS validation.
-   There are a number of supported DNS providers and servers you can utilise,
-   see the <link xlink:href="https://go-acme.github.io/lego/dns/">lego docs</link>
-   for provider/server specific configuration values. For the sake of these
-   docs, we will provide a fully self-hosted example using bind.
-  </para>
-
-<programlisting>
-services.bind = {
-  <link linkend="opt-services.bind.enable">enable</link> = true;
-  <link linkend="opt-services.bind.extraConfig">extraConfig</link> = ''
-    include "/var/lib/secrets/dnskeys.conf";
-  '';
-  <link linkend="opt-services.bind.zones">zones</link> = [
-    rec {
-      name = "example.com";
-      file = "/var/db/bind/${name}";
-      master = true;
-      extraConfig = "allow-update { key rfc2136key.example.com.; };";
-    }
-  ];
-}
-
-# Now we can configure ACME
-<xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
-<xref linkend="opt-security.acme.certs" />."example.com" = {
-  <link linkend="opt-security.acme.certs._name_.domain">domain</link> = "*.example.com";
-  <link linkend="opt-security.acme.certs._name_.dnsProvider">dnsProvider</link> = "rfc2136";
-  <link linkend="opt-security.acme.certs._name_.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret";
-  # We don't need to wait for propagation since this is a local DNS server
-  <link linkend="opt-security.acme.certs._name_.dnsPropagationCheck">dnsPropagationCheck</link> = false;
-};
-</programlisting>
-
-  <para>
-   The <filename>dnskeys.conf</filename> and <filename>certs.secret</filename>
-   must be kept secure and thus you should not keep their contents in your
-   Nix config. Instead, generate them one time with a systemd service:
-  </para>
-
-<programlisting>
-systemd.services.dns-rfc2136-conf = {
-  requiredBy = ["acme-example.com.service" "bind.service"];
-  before = ["acme-example.com.service" "bind.service"];
-  unitConfig = {
-    ConditionPathExists = "!/var/lib/secrets/dnskeys.conf";
-  };
-  serviceConfig = {
-    Type = "oneshot";
-    UMask = 0077;
-  };
-  path = [ pkgs.bind ];
-  script = ''
-    mkdir -p /var/lib/secrets
-    chmod 755 /var/lib/secrets
-    tsig-keygen rfc2136key.example.com &gt; /var/lib/secrets/dnskeys.conf
-    chown named:root /var/lib/secrets/dnskeys.conf
-    chmod 400 /var/lib/secrets/dnskeys.conf
-
-    # extract secret value from the dnskeys.conf
-    while read x y; do if [ "$x" = "secret" ]; then secret="''${y:1:''${#y}-3}"; fi; done &lt; /var/lib/secrets/dnskeys.conf
-
-    cat &gt; /var/lib/secrets/certs.secret &lt;&lt; EOF
-    RFC2136_NAMESERVER='127.0.0.1:53'
-    RFC2136_TSIG_ALGORITHM='hmac-sha256.'
-    RFC2136_TSIG_KEY='rfc2136key.example.com'
-    RFC2136_TSIG_SECRET='$secret'
-    EOF
-    chmod 400 /var/lib/secrets/certs.secret
-  '';
-};
-</programlisting>
-
-  <para>
-   Now you're all set to generate certs! You should monitor the first invocation
-   by running <literal>systemctl start acme-example.com.service &amp;
-   journalctl -fu acme-example.com.service</literal> and watching its log output.
-  </para>
- </section>
-
- <section xml:id="module-security-acme-config-dns-with-vhosts">
-  <title>Using DNS validation with web server virtual hosts</title>
-
-  <para>
-   It is possible to use DNS-01 validation with all certificates,
-   including those automatically configured via the Nginx/Apache
-   <literal><link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link></literal>
-   option. This configuration pattern is fully
-   supported and part of the module's test suite for Nginx + Apache.
-  </para>
-
-  <para>
-   You must follow the guide above on configuring DNS-01 validation
-   first, however instead of setting the options for one certificate
-   (e.g. <xref linkend="opt-security.acme.certs._name_.dnsProvider" />)
-   you will set them as defaults
-   (e.g. <xref linkend="opt-security.acme.defaults.dnsProvider" />).
-  </para>
-
-<programlisting>
-# Configure ACME appropriately
-<xref linkend="opt-security.acme.acceptTerms" /> = true;
-<xref linkend="opt-security.acme.defaults.email" /> = "admin+acme@example.com";
-<xref linkend="opt-security.acme.defaults" /> = {
-  <link linkend="opt-security.acme.defaults.dnsProvider">dnsProvider</link> = "rfc2136";
-  <link linkend="opt-security.acme.defaults.credentialsFile">credentialsFile</link> = "/var/lib/secrets/certs.secret";
-  # We don't need to wait for propagation since this is a local DNS server
-  <link linkend="opt-security.acme.defaults.dnsPropagationCheck">dnsPropagationCheck</link> = false;
-};
-
-# For each virtual host you would like to use DNS-01 validation with,
-# set acmeRoot = null
-services.nginx = {
-  <link linkend="opt-services.nginx.enable">enable</link> = true;
-  <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
-    "foo.example.com" = {
-      <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">enableACME</link> = true;
-      <link linkend="opt-services.nginx.virtualHosts._name_.acmeRoot">acmeRoot</link> = null;
-    };
-  };
-}
-</programlisting>
-
-  <para>
-   And that's it! Next time your configuration is rebuilt, or when
-   you add a new virtualHost, it will be DNS-01 validated.
-  </para>
- </section>
-
- <section xml:id="module-security-acme-root-owned">
-  <title>Using ACME with services demanding root owned certificates</title>
-
-  <para>
-   Some services refuse to start if the configured certificate files
-   are not owned by root. PostgreSQL and OpenSMTPD are examples of these.
-   There is no way to change the user the ACME module uses (it will always be
-   <literal>acme</literal>), however you can use systemd's
-   <literal>LoadCredential</literal> feature to resolve this elegantly.
-   Below is an example configuration for OpenSMTPD, but this pattern
-   can be applied to any service.
-  </para>
-
-<programlisting>
-# Configure ACME however you like (DNS or HTTP validation), adding
-# the following configuration for the relevant certificate.
-# Note: You cannot use `systemctl reload` here as that would mean
-# the LoadCredential configuration below would be skipped and
-# the service would continue to use old certificates.
-security.acme.certs."mail.example.com".postRun = ''
-  systemctl restart opensmtpd
-'';
-
-# Now you must augment OpenSMTPD's systemd service to load
-# the certificate files.
-<link linkend="opt-systemd.services._name_.requires">systemd.services.opensmtpd.requires</link> = ["acme-finished-mail.example.com.target"];
-<link linkend="opt-systemd.services._name_.serviceConfig">systemd.services.opensmtpd.serviceConfig.LoadCredential</link> = let
-  certDir = config.security.acme.certs."mail.example.com".directory;
-in [
-  "cert.pem:${certDir}/cert.pem"
-  "key.pem:${certDir}/key.pem"
-];
-
-# Finally, configure OpenSMTPD to use these certs.
-services.opensmtpd = let
-  credsDir = "/run/credentials/opensmtpd.service";
-in {
-  enable = true;
-  setSendmail = false;
-  serverConfiguration = ''
-    pki mail.example.com cert "${credsDir}/cert.pem"
-    pki mail.example.com key "${credsDir}/key.pem"
-    listen on localhost tls pki mail.example.com
-    action act1 relay host smtp://127.0.0.1:10027
-    match for local action act1
-  '';
-};
-</programlisting>
- </section>
-
- <section xml:id="module-security-acme-regenerate">
-  <title>Regenerating certificates</title>
-
-  <para>
-   Should you need to regenerate a particular certificate in a hurry, such
-   as when a vulnerability is found in Let's Encrypt, there is now a convenient
-   mechanism for doing so. Running
-   <literal>systemctl clean --what=state acme-example.com.service</literal>
-   will remove all certificate files and the account data for the given domain,
-   allowing you to then <literal>systemctl start acme-example.com.service</literal>
-   to generate fresh ones.
-  </para>
- </section>
- <section xml:id="module-security-acme-fix-jws">
-  <title>Fixing JWS Verification error</title>
-
-  <para>
-   It is possible that your account credentials file may become corrupt and need
-   to be regenerated. In this scenario lego will produce the error <literal>JWS verification error</literal>.
-   The solution is to simply delete the associated accounts file and
-   re-run the affected service(s).
-  </para>
-
-<programlisting>
-# Find the accounts folder for the certificate
-systemctl cat acme-example.com.service | grep -Po 'accounts/[^:]*'
-export accountdir="$(!!)"
-# Move this folder to some place else
-mv /var/lib/acme/.lego/$accountdir{,.bak}
-# Recreate the folder using systemd-tmpfiles
-systemd-tmpfiles --create
-# Get a new account and reissue certificates
-# Note: Do this for all certs that share the same account email address
-systemctl start acme-example.com.service
-</programlisting>
-
- </section>
-</chapter>
diff --git a/nixos/modules/security/audit.nix b/nixos/modules/security/audit.nix
index 06b4766c8f5..afc7dd13039 100644
--- a/nixos/modules/security/audit.nix
+++ b/nixos/modules/security/audit.nix
@@ -57,7 +57,7 @@ in {
         type        = types.enum [ false true "lock" ];
         default     = false;
         description = lib.mdDoc ''
-          Whether to enable the Linux audit system. The special `lock' value can be used to
+          Whether to enable the Linux audit system. The special `lock` value can be used to
           enable auditing and prevent disabling it until a restart. Be careful about locking
           this, as it will prevent you from changing your audit configuration until you
           restart. If possible, test your configuration using build-vm beforehand.
diff --git a/nixos/modules/security/doas.nix b/nixos/modules/security/doas.nix
index 4d15ed9a802..115ca33efb5 100644
--- a/nixos/modules/security/doas.nix
+++ b/nixos/modules/security/doas.nix
@@ -19,7 +19,7 @@ let
   ];
 
   mkArgs = rule:
-    if (isNull rule.args) then ""
+    if (rule.args == null) then ""
     else if (length rule.args == 0) then "args"
     else "args ${concatStringsSep " " rule.args}";
 
@@ -27,9 +27,9 @@ let
     let
       opts = mkOpts rule;
 
-      as = optionalString (!isNull rule.runAs) "as ${rule.runAs}";
+      as = optionalString (rule.runAs != null) "as ${rule.runAs}";
 
-      cmd = optionalString (!isNull rule.cmd) "cmd ${rule.cmd}";
+      cmd = optionalString (rule.cmd != null) "cmd ${rule.cmd}";
 
       args = mkArgs rule;
     in
@@ -75,7 +75,9 @@ in
         {file}`/etc/doas.conf` file. More specific rules should
         come after more general ones in order to yield the expected behavior.
         You can use `mkBefore` and/or `mkAfter` to ensure
-        this is the case when configuration options are merged.
+        this is the case when configuration options are merged. Be aware that
+        this option cannot be used to override the behaviour allowing
+        passwordless operation for root.
       '';
       example = literalExpression ''
         [
@@ -224,7 +226,9 @@ in
       type = with types; lines;
       default = "";
       description = lib.mdDoc ''
-        Extra configuration text appended to {file}`doas.conf`.
+        Extra configuration text appended to {file}`doas.conf`. Be aware that
+        this option cannot be used to override the behaviour allowing
+        passwordless operation for root.
       '';
     };
   };
@@ -266,14 +270,14 @@ in
             # completely replace the contents of this file, use
             # `environment.etc."doas.conf"`.
 
-            # "root" is allowed to do anything.
-            permit nopass keepenv root
-
             # extraRules
             ${concatStringsSep "\n" (lists.flatten (map mkRule cfg.extraRules))}
 
             # extraConfig
             ${cfg.extraConfig}
+
+            # "root" is allowed to do anything.
+            permit nopass keepenv root
           '';
           preferLocalBuild = true;
         }
diff --git a/nixos/modules/security/ipa.nix b/nixos/modules/security/ipa.nix
new file mode 100644
index 00000000000..7075be95040
--- /dev/null
+++ b/nixos/modules/security/ipa.nix
@@ -0,0 +1,258 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+with lib; let
+  cfg = config.security.ipa;
+  pyBool = x:
+    if x
+    then "True"
+    else "False";
+
+  ldapConf = pkgs.writeText "ldap.conf" ''
+    # Turning this off breaks GSSAPI used with krb5 when rdns = false
+    SASL_NOCANON    on
+
+    URI ldaps://${cfg.server}
+    BASE ${cfg.basedn}
+    TLS_CACERT /etc/ipa/ca.crt
+  '';
+  nssDb =
+    pkgs.runCommand "ipa-nssdb"
+    {
+      nativeBuildInputs = [pkgs.nss.tools];
+    } ''
+      mkdir -p $out
+      certutil -d $out -N --empty-password
+      certutil -d $out -A --empty-password -n "${cfg.realm} IPA CA" -t CT,C,C -i ${cfg.certificate}
+    '';
+in {
+  options = {
+    security.ipa = {
+      enable = mkEnableOption (lib.mdDoc "FreeIPA domain integration");
+
+      certificate = mkOption {
+        type = types.package;
+        description = lib.mdDoc ''
+          IPA server CA certificate.
+
+          Use `nix-prefetch-url http://$server/ipa/config/ca.crt` to
+          obtain the file and the hash.
+        '';
+        example = literalExpression ''
+          pkgs.fetchurl {
+            url = http://ipa.example.com/ipa/config/ca.crt;
+            sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+          };
+        '';
+      };
+
+      domain = mkOption {
+        type = types.str;
+        example = "example.com";
+        description = lib.mdDoc "Domain of the IPA server.";
+      };
+
+      realm = mkOption {
+        type = types.str;
+        example = "EXAMPLE.COM";
+        description = lib.mdDoc "Kerberos realm.";
+      };
+
+      server = mkOption {
+        type = types.str;
+        example = "ipa.example.com";
+        description = lib.mdDoc "IPA Server hostname.";
+      };
+
+      basedn = mkOption {
+        type = types.str;
+        example = "dc=example,dc=com";
+        description = lib.mdDoc "Base DN to use when performing LDAP operations.";
+      };
+
+      offlinePasswords = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Whether to store offline passwords when the server is down.";
+      };
+
+      cacheCredentials = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Whether to cache credentials.";
+      };
+
+      ifpAllowedUids = mkOption {
+        type = types.listOf types.string;
+        default = ["root"];
+        description = lib.mdDoc "A list of users allowed to access the ifp dbus interface.";
+      };
+
+      dyndns = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = lib.mdDoc "Whether to enable FreeIPA automatic hostname updates.";
+        };
+
+        interface = mkOption {
+          type = types.str;
+          example = "eth0";
+          default = "*";
+          description = lib.mdDoc "Network interface to perform hostname updates through.";
+        };
+      };
+
+      chromiumSupport = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Whether to whitelist the FreeIPA domain in Chromium.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !config.krb5.enable;
+        message = "krb5 must be disabled through `krb5.enable` for FreeIPA integration to work.";
+      }
+      {
+        assertion = !config.users.ldap.enable;
+        message = "ldap must be disabled through `users.ldap.enable` for FreeIPA integration to work.";
+      }
+    ];
+
+    environment.systemPackages = with pkgs; [krb5Full freeipa];
+
+    environment.etc = {
+      "ipa/default.conf".text = ''
+        [global]
+        basedn = ${cfg.basedn}
+        realm = ${cfg.realm}
+        domain = ${cfg.domain}
+        server = ${cfg.server}
+        host = ${config.networking.hostName}
+        xmlrpc_uri = https://${cfg.server}/ipa/xml
+        enable_ra = True
+      '';
+
+      "ipa/nssdb".source = nssDb;
+
+      "krb5.conf".text = ''
+        [libdefaults]
+         default_realm = ${cfg.realm}
+         dns_lookup_realm = false
+         dns_lookup_kdc = true
+         rdns = false
+         ticket_lifetime = 24h
+         forwardable = true
+         udp_preference_limit = 0
+
+        [realms]
+         ${cfg.realm} = {
+          kdc = ${cfg.server}:88
+          master_kdc = ${cfg.server}:88
+          admin_server = ${cfg.server}:749
+          default_domain = ${cfg.domain}
+          pkinit_anchors = FILE:/etc/ipa/ca.crt
+        }
+
+        [domain_realm]
+         .${cfg.domain} = ${cfg.realm}
+         ${cfg.domain} = ${cfg.realm}
+         ${cfg.server} = ${cfg.realm}
+
+        [dbmodules]
+          ${cfg.realm} = {
+            db_library = ${pkgs.freeipa}/lib/krb5/plugins/kdb/ipadb.so
+          }
+      '';
+
+      "openldap/ldap.conf".source = ldapConf;
+    };
+
+    environment.etc."chromium/policies/managed/freeipa.json" = mkIf cfg.chromiumSupport {
+      text = ''
+        { "AuthServerWhitelist": "*.${cfg.domain}" }
+      '';
+    };
+
+    system.activationScripts.ipa = stringAfter ["etc"] ''
+      # libcurl requires a hard copy of the certificate
+      if ! ${pkgs.diffutils}/bin/diff ${cfg.certificate} /etc/ipa/ca.crt > /dev/null 2>&1; then
+        rm -f /etc/ipa/ca.crt
+        cp ${cfg.certificate} /etc/ipa/ca.crt
+      fi
+
+      if [ ! -f /etc/krb5.keytab ]; then
+        cat <<EOF
+
+          In order to complete FreeIPA integration, please join the domain by completing the following steps:
+          1. Authenticate as an IPA user authorized to join new hosts, e.g. kinit admin@${cfg.realm}
+          2. Join the domain and obtain the keytab file: ipa-join
+          3. Install the keytab file: sudo install -m 600 krb5.keytab /etc/
+          4. Restart sssd systemd service: sudo systemctl restart sssd
+
+      EOF
+      fi
+    '';
+
+    services.sssd.config = ''
+      [domain/${cfg.domain}]
+      id_provider = ipa
+      auth_provider = ipa
+      access_provider = ipa
+      chpass_provider = ipa
+
+      ipa_domain = ${cfg.domain}
+      ipa_server = _srv_, ${cfg.server}
+      ipa_hostname = ${config.networking.hostName}.${cfg.domain}
+
+      cache_credentials = ${pyBool cfg.cacheCredentials}
+      krb5_store_password_if_offline = ${pyBool cfg.offlinePasswords}
+      ${optionalString ((toLower cfg.domain) != (toLower cfg.realm))
+        "krb5_realm = ${cfg.realm}"}
+
+      dyndns_update = ${pyBool cfg.dyndns.enable}
+      dyndns_iface = ${cfg.dyndns.interface}
+
+      ldap_tls_cacert = /etc/ipa/ca.crt
+      ldap_user_extra_attrs = mail:mail, sn:sn, givenname:givenname, telephoneNumber:telephoneNumber, lock:nsaccountlock
+
+      [sssd]
+      debug_level = 65510
+      services = nss, sudo, pam, ssh, ifp
+      domains = ${cfg.domain}
+
+      [nss]
+      homedir_substring = /home
+
+      [pam]
+      pam_pwd_expiration_warning = 3
+      pam_verbosity = 3
+
+      [sudo]
+      debug_level = 65510
+
+      [autofs]
+
+      [ssh]
+
+      [pac]
+
+      [ifp]
+      user_attributes = +mail, +telephoneNumber, +givenname, +sn, +lock
+      allowed_uids = ${concatStringsSep ", " cfg.ifpAllowedUids}
+    '';
+
+    services.ntp.servers = singleton cfg.server;
+    services.sssd.enable = true;
+    services.ntp.enable = true;
+
+    security.pki.certificateFiles = singleton cfg.certificate;
+  };
+}
diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix
index 273bc796341..6e8be412de8 100644
--- a/nixos/modules/security/pam.nix
+++ b/nixos/modules/security/pam.nix
@@ -488,6 +488,9 @@ let
             account [success=ok ignore=ignore default=die] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so
             account [success=ok default=ignore] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_admin.so
           '' +
+          optionalString config.services.homed.enable ''
+            account sufficient ${config.systemd.package}/lib/security/pam_systemd_home.so
+          '' +
           # The required pam_unix.so module has to come after all the sufficient modules
           # because otherwise, the account lookup will fail if the user does not exist
           # locally, for example with MySQL- or LDAP-auth.
@@ -541,8 +544,10 @@ let
           # after it succeeds. Certain modules need to run after pam_unix
           # prompts the user for password so we run it once with 'optional' at an
           # earlier point and it will run again with 'sufficient' further down.
-          # We use try_first_pass the second time to avoid prompting password twice
-          (optionalString (cfg.unixAuth &&
+          # We use try_first_pass the second time to avoid prompting password twice.
+          #
+          # The same principle applies to systemd-homed
+          (optionalString ((cfg.unixAuth || config.services.homed.enable) &&
             (config.security.pam.enableEcryptfs
               || config.security.pam.enableFscrypt
               || cfg.pamMount
@@ -553,7 +558,10 @@ let
               || cfg.failDelay.enable
               || cfg.duoSecurity.enable))
             (
-              ''
+              optionalString config.services.homed.enable ''
+                auth optional ${config.systemd.package}/lib/security/pam_systemd_home.so
+              '' +
+              optionalString cfg.unixAuth ''
                 auth optional pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} ${optionalString cfg.nodelay "nodelay"} likeauth
               '' +
               optionalString config.security.pam.enableEcryptfs ''
@@ -584,6 +592,9 @@ let
                 auth required ${pkgs.duo-unix}/lib/security/pam_duo.so
               ''
             )) +
+          optionalString config.services.homed.enable ''
+            auth sufficient ${config.systemd.package}/lib/security/pam_systemd_home.so
+          '' +
           optionalString cfg.unixAuth ''
             auth sufficient pam_unix.so ${optionalString cfg.allowNullPassword "nullok"} ${optionalString cfg.nodelay "nodelay"} likeauth try_first_pass
           '' +
@@ -605,7 +616,11 @@ let
             auth required pam_deny.so
 
             # Password management.
-            password sufficient pam_unix.so nullok sha512
+          '' +
+          optionalString config.services.homed.enable ''
+            password sufficient ${config.systemd.package}/lib/security/pam_systemd_home.so
+          '' + ''
+            password sufficient pam_unix.so nullok yescrypt
           '' +
           optionalString config.security.pam.enableEcryptfs ''
             password optional ${pkgs.ecryptfs}/lib/security/pam_ecryptfs.so
@@ -650,6 +665,9 @@ let
           ++ optional (cfg.ttyAudit.enablePattern != null) "enable=${cfg.ttyAudit.enablePattern}"
           ++ optional (cfg.ttyAudit.disablePattern != null) "disable=${cfg.ttyAudit.disablePattern}"
           )) +
+          optionalString config.services.homed.enable ''
+            session required ${config.systemd.package}/lib/security/pam_systemd_home.so
+          '' +
           optionalString cfg.makeHomeDir ''
             session required ${pkgs.pam}/lib/security/pam_mkhomedir.so silent skel=${config.security.pam.makeHomeDir.skelDirectory} umask=0077
           '' +
@@ -775,7 +793,7 @@ let
     };
   }));
 
-  motd = if isNull config.users.motdFile
+  motd = if config.users.motdFile == null
          then pkgs.writeText "motd" config.users.motd
          else config.users.motdFile;
 
@@ -1215,7 +1233,7 @@ in
   config = {
     assertions = [
       {
-        assertion = isNull config.users.motd || isNull config.users.motdFile;
+        assertion = config.users.motd == null || config.users.motdFile == null;
         message = ''
           Only one of users.motd and users.motdFile can be set.
         '';
@@ -1361,6 +1379,9 @@ in
       '' +
       optionalString config.virtualisation.lxc.lxcfs.enable ''
         mr ${pkgs.lxc}/lib/security/pam_cgfs.so
+      '' +
+      optionalString config.services.homed.enable ''
+        mr ${config.systemd.package}/lib/security/pam_systemd_home.so
       '';
   };
 
diff --git a/nixos/modules/security/polkit.nix b/nixos/modules/security/polkit.nix
index f33898578b8..de427ccb295 100644
--- a/nixos/modules/security/polkit.nix
+++ b/nixos/modules/security/polkit.nix
@@ -14,7 +14,7 @@ in
 
     security.polkit.enable = mkEnableOption (lib.mdDoc "polkit");
 
-    security.polkit.debug = mkEnableOption (lib.mdDoc "debug logs from polkit. This is required in order to see log messages from rule definitions.");
+    security.polkit.debug = mkEnableOption (lib.mdDoc "debug logs from polkit. This is required in order to see log messages from rule definitions");
 
     security.polkit.extraConfig = mkOption {
       type = types.lines;
diff --git a/nixos/modules/security/systemd-confinement.nix b/nixos/modules/security/systemd-confinement.nix
index be04741f4d0..cdf6c22ef1b 100644
--- a/nixos/modules/security/systemd-confinement.nix
+++ b/nixos/modules/security/systemd-confinement.nix
@@ -94,7 +94,6 @@ in {
       };
 
       config = let
-        rootName = "${mkPathSafeName name}-chroot";
         inherit (config.confinement) binSh fullUnit;
         wantsAPIVFS = lib.mkDefault (config.confinement.mode == "full-apivfs");
       in lib.mkIf config.confinement.enable {
diff --git a/nixos/modules/services/amqp/activemq/default.nix b/nixos/modules/services/amqp/activemq/default.nix
index bd37fe3b557..b1f9b7a3bb1 100644
--- a/nixos/modules/services/amqp/activemq/default.nix
+++ b/nixos/modules/services/amqp/activemq/default.nix
@@ -7,20 +7,19 @@ let
 
   cfg = config.services.activemq;
 
-  activemqBroker = stdenv.mkDerivation {
-    name = "activemq-broker";
-    phases = [ "installPhase" ];
-    buildInputs = [ jdk ];
-    installPhase = ''
-      mkdir -p $out/lib
-      source ${activemq}/lib/classpath.env
-      export CLASSPATH
-      ln -s "${./ActiveMQBroker.java}" ActiveMQBroker.java
-      javac -d $out/lib ActiveMQBroker.java
-    '';
-  };
+  activemqBroker = runCommand "activemq-broker"
+    {
+      nativeBuildInputs = [ jdk ];
+    } ''
+    mkdir -p $out/lib
+    source ${activemq}/lib/classpath.env
+    export CLASSPATH
+    ln -s "${./ActiveMQBroker.java}" ActiveMQBroker.java
+    javac -d $out/lib ActiveMQBroker.java
+  '';
 
-in {
+in
+{
 
   options = {
     services.activemq = {
diff --git a/nixos/modules/services/audio/gmediarender.nix b/nixos/modules/services/audio/gmediarender.nix
new file mode 100644
index 00000000000..2f23232d19c
--- /dev/null
+++ b/nixos/modules/services/audio/gmediarender.nix
@@ -0,0 +1,116 @@
+{ pkgs, lib, config, utils, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gmediarender;
+in
+{
+  options.services.gmediarender = {
+    enable = mkEnableOption (mdDoc "the gmediarender DLNA renderer");
+
+    audioDevice = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = mdDoc ''
+        The audio device to use.
+      '';
+    };
+
+    audioSink = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = mdDoc ''
+        The audio sink to use.
+      '';
+    };
+
+    friendlyName = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = mdDoc ''
+        A "friendly name" for identifying the endpoint.
+      '';
+    };
+
+    initialVolume = mkOption {
+      type = types.nullOr types.int;
+      default = 0;
+      description = mdDoc ''
+        A default volume attenuation (in dB) for the endpoint.
+      '';
+    };
+
+    package = mkPackageOptionMD pkgs "gmediarender" {
+      default = "gmrender-resurrect";
+    };
+
+    port = mkOption {
+      type = types.nullOr types.port;
+      default = null;
+      description = mdDoc "Port that will be used to accept client connections.";
+    };
+
+    uuid = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = mdDoc ''
+        A UUID for uniquely identifying the endpoint.  If you have
+        multiple renderers on your network, you MUST set this.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd = {
+      services.gmediarender = {
+        after = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+        description = "gmediarender server daemon";
+        environment = {
+          XDG_CACHE_HOME = "%t/gmediarender";
+        };
+        serviceConfig = {
+          DynamicUser = true;
+          User = "gmediarender";
+          Group = "gmediarender";
+          SupplementaryGroups = [ "audio" ];
+          ExecStart =
+            "${cfg.package}/bin/gmediarender " +
+            optionalString (cfg.audioDevice != null) ("--gstout-audiodevice=${utils.escapeSystemdExecArg cfg.audioDevice} ") +
+            optionalString (cfg.audioSink != null) ("--gstout-audiosink=${utils.escapeSystemdExecArg cfg.audioSink} ") +
+            optionalString (cfg.friendlyName != null) ("--friendly-name=${utils.escapeSystemdExecArg cfg.friendlyName} ") +
+            optionalString (cfg.initialVolume != 0) ("--initial-volume=${toString cfg.initialVolume} ") +
+            optionalString (cfg.port != null) ("--port=${toString cfg.port} ") +
+            optionalString (cfg.uuid != null) ("--uuid=${utils.escapeSystemdExecArg cfg.uuid} ");
+          Restart = "always";
+          RuntimeDirectory = "gmediarender";
+
+          # Security options:
+          CapabilityBoundingSet = "";
+          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";
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@privileged" ];
+          UMask = 066;
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/audio/gonic.nix b/nixos/modules/services/audio/gonic.nix
new file mode 100644
index 00000000000..65cf10f2c4b
--- /dev/null
+++ b/nixos/modules/services/audio/gonic.nix
@@ -0,0 +1,89 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.gonic;
+  settingsFormat = pkgs.formats.keyValue {
+    mkKeyValue = lib.generators.mkKeyValueDefault { } " ";
+    listsAsDuplicateKeys = true;
+  };
+in
+{
+  options = {
+    services.gonic = {
+
+      enable = mkEnableOption (lib.mdDoc "Gonic music server");
+
+      settings = mkOption rec {
+        type = settingsFormat.type;
+        apply = recursiveUpdate default;
+        default = {
+          listen-addr = "127.0.0.1:4747";
+          cache-path = "/var/cache/gonic";
+          tls-cert = null;
+          tls-key = null;
+        };
+        example = {
+          music-path = [ "/mnt/music" ];
+          podcast-path = "/mnt/podcasts";
+        };
+        description = lib.mdDoc ''
+          Configuration for Gonic, see <https://github.com/sentriz/gonic#configuration-options> for supported values.
+        '';
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.gonic = {
+      description = "Gonic Media Server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart =
+          let
+            # these values are null by default but should not appear in the final config
+            filteredSettings = filterAttrs (n: v: !((n == "tls-cert" || n == "tls-key") && v == null)) cfg.settings;
+          in
+          "${pkgs.gonic}/bin/gonic -config-path ${settingsFormat.generate "gonic" filteredSettings}";
+        DynamicUser = true;
+        StateDirectory = "gonic";
+        CacheDirectory = "gonic";
+        WorkingDirectory = "/var/lib/gonic";
+        RuntimeDirectory = "gonic";
+        RootDirectory = "/run/gonic";
+        ReadWritePaths = "";
+        BindReadOnlyPaths = [
+          # gonic can access scrobbling services
+          "-/etc/ssl/certs/ca-certificates.crt"
+          builtins.storeDir
+          cfg.settings.podcast-path
+        ] ++ cfg.settings.music-path
+        ++ lib.optional (cfg.settings.tls-cert != null) cfg.settings.tls-cert
+        ++ lib.optional (cfg.settings.tls-key != null) cfg.settings.tls-key;
+        CapabilityBoundingSet = "";
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
+        RestrictRealtime = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        UMask = "0066";
+        ProtectHostname = true;
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.autrimpo ];
+}
diff --git a/nixos/modules/services/audio/hqplayerd.nix b/nixos/modules/services/audio/hqplayerd.nix
index eff1549380c..d54400b18e3 100644
--- a/nixos/modules/services/audio/hqplayerd.nix
+++ b/nixos/modules/services/audio/hqplayerd.nix
@@ -82,7 +82,6 @@ in
       etc = {
         "hqplayer/hqplayerd.xml" = mkIf (cfg.config != null) { source = pkgs.writeText "hqplayerd.xml" cfg.config; };
         "hqplayer/hqplayerd4-key.xml" = mkIf (cfg.licenseFile != null) { source = cfg.licenseFile; };
-        "modules-load.d/taudio2.conf".source = "${pkg}/etc/modules-load.d/taudio2.conf";
       };
       systemPackages = [ pkg ];
     };
@@ -91,8 +90,6 @@ in
       allowedTCPPorts = [ 8088 4321 ];
     };
 
-    services.udev.packages = [ pkg ];
-
     systemd = {
       tmpfiles.rules = [
         "d ${configDir}      0755 hqplayer hqplayer - -"
diff --git a/nixos/modules/services/audio/mpd.nix b/nixos/modules/services/audio/mpd.nix
index ba1e4716c9b..3c853973c87 100644
--- a/nixos/modules/services/audio/mpd.nix
+++ b/nixos/modules/services/audio/mpd.nix
@@ -102,7 +102,7 @@ in {
           Extra directives added to to the end of MPD's configuration file,
           mpd.conf. Basic configuration like file location and uid/gid
           is added automatically to the beginning of the file. For available
-          options see `man 5 mpd.conf`'.
+          options see {manpage}`mpd.conf(5)`.
         '';
       };
 
diff --git a/nixos/modules/services/audio/navidrome.nix b/nixos/modules/services/audio/navidrome.nix
index e73828081d4..e18e61eb6d4 100644
--- a/nixos/modules/services/audio/navidrome.nix
+++ b/nixos/modules/services/audio/navidrome.nix
@@ -11,6 +11,8 @@ in {
 
       enable = mkEnableOption (lib.mdDoc "Navidrome music server");
 
+      package = mkPackageOptionMD pkgs "navidrome" { };
+
       settings = mkOption rec {
         type = settingsFormat.type;
         apply = recursiveUpdate default;
@@ -36,7 +38,7 @@ in {
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.navidrome}/bin/navidrome --configfile ${settingsFormat.generate "navidrome.json" cfg.settings}
+          ${cfg.package}/bin/navidrome --configfile ${settingsFormat.generate "navidrome.json" cfg.settings}
         '';
         DynamicUser = true;
         StateDirectory = "navidrome";
diff --git a/nixos/modules/services/audio/roon-bridge.nix b/nixos/modules/services/audio/roon-bridge.nix
index db84ba28622..70392b647cc 100644
--- a/nixos/modules/services/audio/roon-bridge.nix
+++ b/nixos/modules/services/audio/roon-bridge.nix
@@ -42,7 +42,7 @@ in {
       environment.ROON_DATAROOT = "/var/lib/${name}";
 
       serviceConfig = {
-        ExecStart = "${pkgs.roon-bridge}/start.sh";
+        ExecStart = "${pkgs.roon-bridge}/bin/RoonBridge";
         LimitNOFILE = 8192;
         User = cfg.user;
         Group = cfg.group;
@@ -53,13 +53,18 @@ in {
     networking.firewall = mkIf cfg.openFirewall {
       allowedTCPPortRanges = [{ from = 9100; to = 9200; }];
       allowedUDPPorts = [ 9003 ];
-      extraCommands = ''
+      extraCommands = optionalString (!config.networking.nftables.enable) ''
         iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
         iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
         iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT
         iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
         iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
       '';
+      extraInputRules = optionalString config.networking.nftables.enable ''
+        ip saddr { 224.0.0.0/4, 240.0.0.0/5 } accept
+        ip daddr 224.0.0.0/4 accept
+        pkttype { multicast, broadcast } accept
+      '';
     };
 
 
diff --git a/nixos/modules/services/audio/roon-server.nix b/nixos/modules/services/audio/roon-server.nix
index 74cae909f5d..fbe74f63b9d 100644
--- a/nixos/modules/services/audio/roon-server.nix
+++ b/nixos/modules/services/audio/roon-server.nix
@@ -58,7 +58,7 @@ in {
         { from = 30000; to = 30010; }
       ];
       allowedUDPPorts = [ 9003 ];
-      extraCommands = ''
+      extraCommands = optionalString (!config.networking.nftables.enable) ''
         ## IGMP / Broadcast ##
         iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT
         iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT
@@ -66,6 +66,11 @@ in {
         iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT
         iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT
       '';
+      extraInputRules = optionalString config.networking.nftables.enable ''
+        ip saddr { 224.0.0.0/4, 240.0.0.0/5 } accept
+        ip daddr 224.0.0.0/4 accept
+        pkttype { multicast, broadcast } accept
+      '';
     };
 
 
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix
index 2af42eeb370..dbab741bf6f 100644
--- a/nixos/modules/services/audio/snapserver.nix
+++ b/nixos/modules/services/audio/snapserver.nix
@@ -275,9 +275,9 @@ in {
 
     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 ''
+      filter (w: w != "") (mapAttrsToList (k: v: optionalString (v.type == "spotify") ''
         services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead.
-      '' else "") cfg.streams);
+      '') cfg.streams);
 
     systemd.services.snapserver = {
       after = [ "network.target" ];
diff --git a/nixos/modules/services/audio/tts.nix b/nixos/modules/services/audio/tts.nix
new file mode 100644
index 00000000000..1a355c8ee39
--- /dev/null
+++ b/nixos/modules/services/audio/tts.nix
@@ -0,0 +1,151 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.tts;
+in
+
+{
+  options.services.tts = let
+    inherit (lib) literalExpression mkOption mdDoc mkEnableOption types;
+  in  {
+    servers = mkOption {
+      type = types.attrsOf (types.submodule (
+        { ... }: {
+          options = {
+            enable = mkEnableOption (mdDoc "Coqui TTS server");
+
+            port = mkOption {
+              type = types.port;
+              example = 5000;
+              description = mdDoc ''
+                Port to bind the TTS server to.
+              '';
+            };
+
+            model = mkOption {
+              type = types.nullOr types.str;
+              default = "tts_models/en/ljspeech/tacotron2-DDC";
+              example = null;
+              description = mdDoc ''
+                Name of the model to download and use for speech synthesis.
+
+                Check `tts-server --list_models` for possible values.
+
+                Set to `null` to use a custom model.
+              '';
+            };
+
+            useCuda = mkOption {
+              type = types.bool;
+              default = false;
+              example = true;
+              description = mdDoc ''
+                Whether to offload computation onto a CUDA compatible GPU.
+              '';
+            };
+
+            extraArgs = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = mdDoc ''
+                Extra arguments to pass to the server commandline.
+              '';
+            };
+          };
+        }
+      ));
+      default = {};
+      example = literalExpression ''
+        {
+          english = {
+            port = 5300;
+            model = "tts_models/en/ljspeech/tacotron2-DDC";
+          };
+          german = {
+            port = 5301;
+            model = "tts_models/de/thorsten/tacotron2-DDC";
+          };
+          dutch = {
+            port = 5302;
+            model = "tts_models/nl/mai/tacotron2-DDC";
+          };
+        }
+      '';
+      description = mdDoc ''
+        TTS server instances.
+      '';
+    };
+  };
+
+  config = let
+    inherit (lib) mkIf mapAttrs' nameValuePair optionalString concatMapStringsSep escapeShellArgs;
+  in mkIf (cfg.servers != {}) {
+    systemd.services = mapAttrs' (server: options:
+      nameValuePair "tts-${server}" {
+        description = "Coqui TTS server instance ${server}";
+        after = [
+          "network-online.target"
+        ];
+        wantedBy = [
+          "multi-user.target"
+        ];
+        path = with pkgs; [
+          espeak-ng
+        ];
+        environment.HOME = "/var/lib/tts";
+        serviceConfig = {
+          DynamicUser = true;
+          User = "tts";
+          StateDirectory = "tts";
+          ExecStart = "${pkgs.tts}/bin/tts-server --port ${toString options.port}"
+            + optionalString (options.model != null) " --model_name ${options.model}"
+            + optionalString (options.useCuda) " --use_cuda"
+            + (concatMapStringsSep " " escapeShellArgs options.extraArgs);
+          CapabilityBoundingSet = "";
+          DeviceAllow = if options.useCuda then [
+            # https://docs.nvidia.com/dgx/pdf/dgx-os-5-user-guide.pdf
+            "/dev/nvidia1"
+            "/dev/nvidia2"
+            "/dev/nvidia3"
+            "/dev/nvidia4"
+            "/dev/nvidia-caps/nvidia-cap1"
+            "/dev/nvidia-caps/nvidia-cap2"
+            "/dev/nvidiactl"
+            "/dev/nvidia-modeset"
+            "/dev/nvidia-uvm"
+            "/dev/nvidia-uvm-tools"
+          ] else "";
+          DevicePolicy = "closed";
+          LockPersonality = true;
+          # jit via numba->llvmpipe
+          MemoryDenyWriteExecute = false;
+          PrivateDevices = true;
+          PrivateUsers = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectControlGroups = true;
+          ProtectProc = "invisible";
+          ProcSubset = "pid";
+          RestrictAddressFamilies = [
+            "AF_INET"
+            "AF_INET6"
+          ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [
+            "@system-service"
+            "~@privileged"
+          ];
+          UMask = "0077";
+        };
+      }) cfg.servers;
+  };
+}
diff --git a/nixos/modules/services/audio/ympd.nix b/nixos/modules/services/audio/ympd.nix
index 811b81030ef..b74cc3f9c0b 100644
--- a/nixos/modules/services/audio/ympd.nix
+++ b/nixos/modules/services/audio/ympd.nix
@@ -48,8 +48,46 @@ in {
 
     systemd.services.ympd = {
       description = "Standalone MPD Web GUI written in C";
+
       wantedBy = [ "multi-user.target" ];
-      serviceConfig.ExecStart = "${pkgs.ympd}/bin/ympd --host ${cfg.mpd.host} --port ${toString cfg.mpd.port} --webport ${toString cfg.webPort} --user nobody";
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        ExecStart = ''
+          ${pkgs.ympd}/bin/ympd \
+            --host ${cfg.mpd.host} \
+            --port ${toString cfg.mpd.port} \
+            --webport ${toString cfg.webPort}
+        '';
+
+        DynamicUser = true;
+        NoNewPrivileges = true;
+
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ProtectHome = "tmpfs";
+
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateIPC = true;
+
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        SystemCallFilter = [
+          "@system-service"
+          "~@process"
+          "~@setuid"
+        ];
+      };
     };
 
   };
diff --git a/nixos/modules/services/backup/borgbackup.md b/nixos/modules/services/backup/borgbackup.md
new file mode 100644
index 00000000000..39141f6ec85
--- /dev/null
+++ b/nixos/modules/services/backup/borgbackup.md
@@ -0,0 +1,163 @@
+# BorgBackup {#module-borgbase}
+
+*Source:* {file}`modules/services/backup/borgbackup.nix`
+
+*Upstream documentation:* <https://borgbackup.readthedocs.io/>
+
+[BorgBackup](https://www.borgbackup.org/) (short: Borg)
+is a deduplicating backup program. Optionally, it supports compression and
+authenticated encryption.
+
+The main goal of Borg is to provide an efficient and secure way to backup
+data. The data deduplication technique used makes Borg suitable for daily
+backups since only changes are stored. The authenticated encryption technique
+makes it suitable for backups to not fully trusted targets.
+
+## Configuring {#module-services-backup-borgbackup-configuring}
+
+A complete list of options for the Borgbase module may be found
+[here](#opt-services.borgbackup.jobs).
+
+## Basic usage for a local backup {#opt-services-backup-borgbackup-local-directory}
+
+A very basic configuration for backing up to a locally accessible directory is:
+```
+{
+    opt.services.borgbackup.jobs = {
+      { rootBackup = {
+          paths = "/";
+          exclude = [ "/nix" "/path/to/local/repo" ];
+          repo = "/path/to/local/repo";
+          doInit = true;
+          encryption = {
+            mode = "repokey";
+            passphrase = "secret";
+          };
+          compression = "auto,lzma";
+          startAt = "weekly";
+        };
+      }
+    };
+}
+```
+
+::: {.warning}
+If you do not want the passphrase to be stored in the world-readable
+Nix store, use passCommand. You find an example below.
+:::
+
+## Create a borg backup server {#opt-services-backup-create-server}
+
+You should use a different SSH key for each repository you write to,
+because the specified keys are restricted to running borg serve and can only
+access this single repository. You need the output of the generate pub file.
+
+```ShellSession
+# sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_my_borg_repo
+# cat /run/keys/id_ed25519_my_borg_repo
+ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos
+```
+
+Add the following snippet to your NixOS configuration:
+```
+{
+  services.borgbackup.repos = {
+    my_borg_repo = {
+      authorizedKeys = [
+        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos"
+      ] ;
+      path = "/var/lib/my_borg_repo" ;
+    };
+  };
+}
+```
+
+## Backup to the borg repository server {#opt-services-backup-borgbackup-remote-server}
+
+The following NixOS snippet creates an hourly backup to the service
+(on the host nixos) as created in the section above. We assume
+that you have stored a secret passphrasse in the file
+{file}`/run/keys/borgbackup_passphrase`, which should be only
+accessible by root
+
+```
+{
+  services.borgbackup.jobs = {
+    backupToLocalServer = {
+      paths = [ "/etc/nixos" ];
+      doInit = true;
+      repo =  "borg@nixos:." ;
+      encryption = {
+        mode = "repokey-blake2";
+        passCommand = "cat /run/keys/borgbackup_passphrase";
+      };
+      environment = { BORG_RSH = "ssh -i /run/keys/id_ed25519_my_borg_repo"; };
+      compression = "auto,lzma";
+      startAt = "hourly";
+    };
+  };
+};
+```
+
+The following few commands (run as root) let you test your backup.
+```
+> nixos-rebuild switch
+...restarting the following units: polkit.service
+> systemctl restart borgbackup-job-backupToLocalServer
+> sleep 10
+> systemctl restart borgbackup-job-backupToLocalServer
+> export BORG_PASSPHRASE=topSecrect
+> borg list --rsh='ssh -i /run/keys/id_ed25519_my_borg_repo' borg@nixos:.
+nixos-backupToLocalServer-2020-03-30T21:46:17 Mon, 2020-03-30 21:46:19 [84feb97710954931ca384182f5f3cb90665f35cef214760abd7350fb064786ac]
+nixos-backupToLocalServer-2020-03-30T21:46:30 Mon, 2020-03-30 21:46:32 [e77321694ecd160ca2228611747c6ad1be177d6e0d894538898de7a2621b6e68]
+```
+
+## Backup to a hosting service {#opt-services-backup-borgbackup-borgbase}
+
+Several companies offer [(paid) hosting services](https://www.borgbackup.org/support/commercial.html)
+for Borg repositories.
+
+To backup your home directory to borgbase you have to:
+
+  - Generate a SSH key without a password, to access the remote server. E.g.
+
+        sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_borgbase
+
+  - Create the repository on the server by following the instructions for your
+    hosting server.
+  - Initialize the repository on the server. Eg.
+
+        sudo borg init --encryption=repokey-blake2  \
+            --rsh "ssh -i /run/keys/id_ed25519_borgbase" \
+            zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo
+
+  - Add it to your NixOS configuration, e.g.
+
+        {
+            services.borgbackup.jobs = {
+            my_Remote_Backup = {
+                paths = [ "/" ];
+                exclude = [ "/nix" "'**/.cache'" ];
+                repo =  "zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo";
+                  encryption = {
+                  mode = "repokey-blake2";
+                  passCommand = "cat /run/keys/borgbackup_passphrase";
+                };
+                environment = { BORG_RSH = "ssh -i /run/keys/id_ed25519_borgbase"; };
+                compression = "auto,lzma";
+                startAt = "daily";
+            };
+          };
+        }}
+
+## Vorta backup client for the desktop {#opt-services-backup-borgbackup-vorta}
+
+Vorta is a backup client for macOS and Linux desktops. It integrates the
+mighty BorgBackup with your desktop environment to protect your data from
+disk failure, ransomware and theft.
+
+It can be installed in NixOS e.g. by adding `pkgs.vorta`
+to [](#opt-environment.systemPackages).
+
+Details about using Vorta can be found under
+[https://vorta.borgbase.com](https://vorta.borgbase.com/usage) .
diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix
index ae8e1dd8463..08a2967e9c7 100644
--- a/nixos/modules/services/backup/borgbackup.nix
+++ b/nixos/modules/services/backup/borgbackup.nix
@@ -66,6 +66,7 @@ let
       ${mkKeepArgs cfg} \
       ${optionalString (cfg.prune.prefix != null) "--glob-archives ${escapeShellArg "${cfg.prune.prefix}*"}"} \
       $extraPruneArgs
+    borg compact $extraArgs $extraCompactArgs
     ${cfg.postPrune}
   '');
 
@@ -150,8 +151,9 @@ let
         # Ensure that the home directory already exists
         # We can't assert createHome == true because that's not the case for root
         cd "${config.users.users.${cfg.user}.home}"
-        ${install} -d .config/borg
-        ${install} -d .cache/borg
+        # Create each directory separately to prevent root owned parent dirs
+        ${install} -d .config .config/borg
+        ${install} -d .cache .cache/borg
       '' + optionalString (isLocalPath cfg.repo && !cfg.removableDevice) ''
         ${install} -d ${escapeShellArg cfg.repo}
       ''));
@@ -225,7 +227,7 @@ let
 
 in {
   meta.maintainers = with maintainers; [ dotlambda ];
-  meta.doc = ./borgbackup.xml;
+  meta.doc = ./borgbackup.md;
 
   ###### interface
 
@@ -637,6 +639,15 @@ in {
             example = "--save-space";
           };
 
+          extraCompactArgs = mkOption {
+            type = types.str;
+            description = lib.mdDoc ''
+              Additional arguments for {command}`borg compact`.
+              Can also be set at runtime using `$extraCompactArgs`.
+            '';
+            default = "";
+            example = "--cleanup-commits";
+          };
         };
       }
     ));
diff --git a/nixos/modules/services/backup/borgbackup.xml b/nixos/modules/services/backup/borgbackup.xml
deleted file mode 100644
index f38064f8677..00000000000
--- a/nixos/modules/services/backup/borgbackup.xml
+++ /dev/null
@@ -1,209 +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-borgbase">
- <title>BorgBackup</title>
-  <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/services/backup/borgbackup.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://borgbackup.readthedocs.io/"/>
- </para>
- <para>
-  <link xlink:href="https://www.borgbackup.org/">BorgBackup</link> (short: Borg)
-  is a deduplicating backup program. Optionally, it supports compression and
-  authenticated encryption.
-  </para>
-  <para>
-  The main goal of Borg is to provide an efficient and secure way to backup
-  data. The data deduplication technique used makes Borg suitable for daily
-  backups since only changes are stored. The authenticated encryption technique
-  makes it suitable for backups to not fully trusted targets.
- </para>
-  <section xml:id="module-services-backup-borgbackup-configuring">
-  <title>Configuring</title>
-  <para>
-   A complete list of options for the Borgbase module may be found
-   <link linkend="opt-services.borgbackup.jobs">here</link>.
-  </para>
-</section>
- <section xml:id="opt-services-backup-borgbackup-local-directory">
-  <title>Basic usage for a local backup</title>
-
-  <para>
-   A very basic configuration for backing up to a locally accessible directory
-   is:
-<programlisting>
-{
-    opt.services.borgbackup.jobs = {
-      { rootBackup = {
-          paths = "/";
-          exclude = [ "/nix" "/path/to/local/repo" ];
-          repo = "/path/to/local/repo";
-          doInit = true;
-          encryption = {
-            mode = "repokey";
-            passphrase = "secret";
-          };
-          compression = "auto,lzma";
-          startAt = "weekly";
-        };
-      }
-    };
-}</programlisting>
-  </para>
-  <warning>
-    <para>
-        If you do not want the passphrase to be stored in the world-readable
-        Nix store, use passCommand. You find an example below.
-    </para>
-  </warning>
- </section>
-<section xml:id="opt-services-backup-create-server">
-  <title>Create a borg backup server</title>
-  <para>You should use a different SSH key for each repository you write to,
-    because the specified keys are restricted to running borg serve and can only
-    access this single repository. You need the output of the generate pub file.
-  </para>
-    <para>
-<screen>
-<prompt># </prompt>sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_my_borg_repo
-<prompt># </prompt>cat /run/keys/id_ed25519_my_borg_repo
-ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos</screen>
-    </para>
-    <para>
-      Add the following snippet to your NixOS configuration:
-      <programlisting>
-{
-  services.borgbackup.repos = {
-    my_borg_repo = {
-      authorizedKeys = [
-        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID78zmOyA+5uPG4Ot0hfAy+sLDPU1L4AiIoRYEIVbbQ/ root@nixos"
-      ] ;
-      path = "/var/lib/my_borg_repo" ;
-    };
-  };
-}</programlisting>
-    </para>
-</section>
-
- <section xml:id="opt-services-backup-borgbackup-remote-server">
-  <title>Backup to the borg repository server</title>
-  <para>The following NixOS snippet creates an hourly backup to the service
-    (on the host nixos) as created in the section above. We assume
-    that you have stored a secret passphrasse in the file
-    <code>/run/keys/borgbackup_passphrase</code>, which should be only
-    accessible by root
-  </para>
-  <para>
-      <programlisting>
-{
-  services.borgbackup.jobs = {
-    backupToLocalServer = {
-      paths = [ "/etc/nixos" ];
-      doInit = true;
-      repo =  "borg@nixos:." ;
-      encryption = {
-        mode = "repokey-blake2";
-        passCommand = "cat /run/keys/borgbackup_passphrase";
-      };
-      environment = { BORG_RSH = "ssh -i /run/keys/id_ed25519_my_borg_repo"; };
-      compression = "auto,lzma";
-      startAt = "hourly";
-    };
-  };
-};</programlisting>
-  </para>
-  <para>The following few commands (run as root) let you test your backup.
-      <programlisting>
-> nixos-rebuild switch
-...restarting the following units: polkit.service
-> systemctl restart borgbackup-job-backupToLocalServer
-> sleep 10
-> systemctl restart borgbackup-job-backupToLocalServer
-> export BORG_PASSPHRASE=topSecrect
-> borg list --rsh='ssh -i /run/keys/id_ed25519_my_borg_repo' borg@nixos:.
-nixos-backupToLocalServer-2020-03-30T21:46:17 Mon, 2020-03-30 21:46:19 [84feb97710954931ca384182f5f3cb90665f35cef214760abd7350fb064786ac]
-nixos-backupToLocalServer-2020-03-30T21:46:30 Mon, 2020-03-30 21:46:32 [e77321694ecd160ca2228611747c6ad1be177d6e0d894538898de7a2621b6e68]</programlisting>
-    </para>
-</section>
-
- <section xml:id="opt-services-backup-borgbackup-borgbase">
-  <title>Backup to a hosting service</title>
-
-  <para>
-    Several companies offer <link
-      xlink:href="https://www.borgbackup.org/support/commercial.html">(paid)
-      hosting services</link> for Borg repositories.
-  </para>
-  <para>
-    To backup your home directory to borgbase you have to:
-  </para>
-  <itemizedlist>
-  <listitem>
-    <para>
-      Generate a SSH key without a password, to access the remote server. E.g.
-    </para>
-    <para>
-        <programlisting>sudo ssh-keygen -N '' -t ed25519 -f /run/keys/id_ed25519_borgbase</programlisting>
-    </para>
-  </listitem>
-  <listitem>
-    <para>
-      Create the repository on the server by following the instructions for your
-      hosting server.
-    </para>
-  </listitem>
-  <listitem>
-    <para>
-      Initialize the repository on the server. Eg.
-      <programlisting>
-sudo borg init --encryption=repokey-blake2  \
-    -rsh "ssh -i /run/keys/id_ed25519_borgbase" \
-    zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo</programlisting>
-  </para>
-  </listitem>
-  <listitem>
-<para>Add it to your NixOS configuration, e.g.
-<programlisting>
-{
-    services.borgbackup.jobs = {
-    my_Remote_Backup = {
-        paths = [ "/" ];
-        exclude = [ "/nix" "'**/.cache'" ];
-        repo =  "zzz2aaaaa@zzz2aaaaa.repo.borgbase.com:repo";
-          encryption = {
-          mode = "repokey-blake2";
-          passCommand = "cat /run/keys/borgbackup_passphrase";
-        };
-        environment = { BORG_RSH = "ssh -i /run/keys/id_ed25519_borgbase"; };
-        compression = "auto,lzma";
-        startAt = "daily";
-    };
-  };
-}}</programlisting>
-  </para>
-  </listitem>
-</itemizedlist>
- </section>
-  <section xml:id="opt-services-backup-borgbackup-vorta">
-  <title>Vorta backup client for the desktop</title>
-  <para>
-    Vorta is a backup client for macOS and Linux desktops. It integrates the
-    mighty BorgBackup with your desktop environment to protect your data from
-    disk failure, ransomware and theft.
-  </para>
-  <para>
-   It can be installed in NixOS e.g. by adding <package>pkgs.vorta</package>
-   to <xref linkend="opt-environment.systemPackages" />.
-  </para>
-  <para>
-    Details about using Vorta can be found under <link
-      xlink:href="https://vorta.borgbase.com/usage">https://vorta.borgbase.com
-      </link>.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/backup/borgmatic.nix b/nixos/modules/services/backup/borgmatic.nix
index 73c4acda393..5ee036e68c7 100644
--- a/nixos/modules/services/backup/borgmatic.nix
+++ b/nixos/modules/services/backup/borgmatic.nix
@@ -5,44 +5,58 @@ with lib;
 let
   cfg = config.services.borgmatic;
   settingsFormat = pkgs.formats.yaml { };
+
+  cfgType = with types; submodule {
+    freeformType = settingsFormat.type;
+    options.location = {
+      source_directories = mkOption {
+        type = listOf str;
+        description = mdDoc ''
+          List of source directories to backup (required). Globs and
+          tildes are expanded.
+        '';
+        example = [ "/home" "/etc" "/var/log/syslog*" ];
+      };
+      repositories = mkOption {
+        type = listOf str;
+        description = mdDoc ''
+          Paths to local or remote repositories (required). Tildes are
+          expanded. Multiple repositories are backed up to in
+          sequence. Borg placeholders can be used. See the output of
+          "borg help placeholders" for details. See ssh_command for
+          SSH options like identity file or port. If systemd service
+          is used, then add local repository paths in the systemd
+          service file to the ReadWritePaths list.
+        '';
+        example = [
+          "ssh://user@backupserver/./sourcehostname.borg"
+          "ssh://user@backupserver/./{fqdn}"
+          "/var/local/backups/local.borg"
+        ];
+      };
+    };
+  };
+
   cfgfile = settingsFormat.generate "config.yaml" cfg.settings;
-in {
+in
+{
   options.services.borgmatic = {
-    enable = mkEnableOption (lib.mdDoc "borgmatic");
+    enable = mkEnableOption (mdDoc "borgmatic");
 
     settings = mkOption {
-      description = lib.mdDoc ''
+      description = mdDoc ''
         See https://torsion.org/borgmatic/docs/reference/configuration/
       '';
-      type = types.submodule {
-        freeformType = settingsFormat.type;
-        options.location = {
-          source_directories = mkOption {
-            type = types.listOf types.str;
-            description = lib.mdDoc ''
-              List of source directories to backup (required). Globs and
-              tildes are expanded.
-            '';
-            example = [ "/home" "/etc" "/var/log/syslog*" ];
-          };
-          repositories = mkOption {
-            type = types.listOf types.str;
-            description = lib.mdDoc ''
-              Paths to local or remote repositories (required). Tildes are
-              expanded. Multiple repositories are backed up to in
-              sequence. Borg placeholders can be used. See the output of
-              "borg help placeholders" for details. See ssh_command for
-              SSH options like identity file or port. If systemd service
-              is used, then add local repository paths in the systemd
-              service file to the ReadWritePaths list.
-            '';
-            example = [
-              "user@backupserver:sourcehostname.borg"
-              "user@backupserver:{fqdn}"
-            ];
-          };
-        };
-      };
+      default = null;
+      type = types.nullOr cfgType;
+    };
+
+    configurations = mkOption {
+      description = mdDoc ''
+        Set of borgmatic configurations, see https://torsion.org/borgmatic/docs/reference/configuration/
+      '';
+      default = { };
+      type = types.attrsOf cfgType;
     };
   };
 
@@ -50,9 +64,16 @@ in {
 
     environment.systemPackages = [ pkgs.borgmatic ];
 
-    environment.etc."borgmatic/config.yaml".source = cfgfile;
+    environment.etc = (optionalAttrs (cfg.settings != null) { "borgmatic/config.yaml".source = cfgfile; }) //
+      mapAttrs'
+        (name: value: nameValuePair
+          "borgmatic.d/${name}.yaml"
+          { source = settingsFormat.generate "${name}.yaml" value; })
+        cfg.configurations;
 
     systemd.packages = [ pkgs.borgmatic ];
 
+    # Workaround: https://github.com/NixOS/nixpkgs/issues/81138
+    systemd.timers.borgmatic.wantedBy = [ "timers.target" ];
   };
 }
diff --git a/nixos/modules/services/backup/btrbk.nix b/nixos/modules/services/backup/btrbk.nix
index b6eb68cc43f..b838c174553 100644
--- a/nixos/modules/services/backup/btrbk.nix
+++ b/nixos/modules/services/backup/btrbk.nix
@@ -47,7 +47,12 @@ let
     then [ "${name} ${value}" ]
     else concatLists (mapAttrsToList (genSection name) value);
 
-  addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings;
+  sudo_doas =
+    if config.security.sudo.enable then "sudo"
+    else if config.security.doas.enable then "doas"
+    else throw "The btrbk nixos module needs either sudo or doas enabled in the configuration";
+
+  addDefaults = settings: { backend = "btrfs-progs-${sudo_doas}"; } // settings;
 
   mkConfigFile = name: settings: pkgs.writeTextFile {
     name = "btrbk-${name}.conf";
@@ -152,20 +157,41 @@ in
   };
   config = mkIf (sshEnabled || serviceEnabled) {
     environment.systemPackages = [ pkgs.btrbk ] ++ cfg.extraPackages;
-    security.sudo.extraRules = [
-      {
-        users = [ "btrbk" ];
-        commands = [
-          { command = "${pkgs.btrfs-progs}/bin/btrfs"; options = [ "NOPASSWD" ]; }
-          { command = "${pkgs.coreutils}/bin/mkdir"; options = [ "NOPASSWD" ]; }
-          { command = "${pkgs.coreutils}/bin/readlink"; options = [ "NOPASSWD" ]; }
-          # for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
-          { command = "/run/current-system/bin/btrfs"; options = [ "NOPASSWD" ]; }
-          { command = "/run/current-system/sw/bin/mkdir"; options = [ "NOPASSWD" ]; }
-          { command = "/run/current-system/sw/bin/readlink"; options = [ "NOPASSWD" ]; }
+    security.sudo = mkIf (sudo_doas == "sudo") {
+      extraRules = [
+        {
+            users = [ "btrbk" ];
+            commands = [
+            { command = "${pkgs.btrfs-progs}/bin/btrfs"; options = [ "NOPASSWD" ]; }
+            { command = "${pkgs.coreutils}/bin/mkdir"; options = [ "NOPASSWD" ]; }
+            { command = "${pkgs.coreutils}/bin/readlink"; options = [ "NOPASSWD" ]; }
+            # for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
+            { command = "/run/current-system/bin/btrfs"; options = [ "NOPASSWD" ]; }
+            { command = "/run/current-system/sw/bin/mkdir"; options = [ "NOPASSWD" ]; }
+            { command = "/run/current-system/sw/bin/readlink"; options = [ "NOPASSWD" ]; }
+            ];
+        }
+      ];
+    };
+    security.doas = mkIf (sudo_doas == "doas") {
+      extraRules = let
+        doasCmdNoPass = cmd: { users = [ "btrbk" ]; cmd = cmd; noPass = true; };
+      in
+        [
+            (doasCmdNoPass "${pkgs.btrfs-progs}/bin/btrfs")
+            (doasCmdNoPass "${pkgs.coreutils}/bin/mkdir")
+            (doasCmdNoPass "${pkgs.coreutils}/bin/readlink")
+            # for ssh, they are not the same than the one hard coded in ${pkgs.btrbk}
+            (doasCmdNoPass "/run/current-system/bin/btrfs")
+            (doasCmdNoPass "/run/current-system/sw/bin/mkdir")
+            (doasCmdNoPass "/run/current-system/sw/bin/readlink")
+
+            # doas matches command, not binary
+            (doasCmdNoPass "btrfs")
+            (doasCmdNoPass "mkdir")
+            (doasCmdNoPass "readlink")
         ];
-      }
-    ];
+    };
     users.users.btrbk = {
       isSystemUser = true;
       # ssh needs a home directory
@@ -183,8 +209,9 @@ in
               "best-effort" = 2;
               "realtime" = 1;
             }.${cfg.ioSchedulingClass};
+            sudo_doas_flag = "--${sudo_doas}";
           in
-          ''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}''
+          ''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_doas_flag} ${options}" ${v.key}''
         )
         cfg.sshAccess;
     };
diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix
index 289291c6bd2..9fbc599cd41 100644
--- a/nixos/modules/services/backup/mysql-backup.nix
+++ b/nixos/modules/services/backup/mysql-backup.nix
@@ -20,7 +20,7 @@ let
   '';
   backupDatabaseScript = db: ''
     dest="${cfg.location}/${db}.gz"
-    if ${mariadb}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then
+    if ${mariadb}/bin/mysqldump ${optionalString cfg.singleTransaction "--single-transaction"} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then
       mv $dest.tmp $dest
       echo "Backed up to $dest"
     else
diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix
index 869ed5d9976..8cc0c084d65 100644
--- a/nixos/modules/services/backup/restic.nix
+++ b/nixos/modules/services/backup/restic.nix
@@ -126,13 +126,28 @@ in
           ];
         };
 
+        exclude = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          description = lib.mdDoc ''
+            Patterns to exclude when backing up. See
+            https://restic.readthedocs.io/en/latest/040_backup.html#excluding-files for
+            details on syntax.
+          '';
+          example = [
+            "/var/cache"
+            "/home/*/.cache"
+            ".git"
+          ];
+        };
+
         timerConfig = mkOption {
           type = types.attrsOf unitOption;
           default = {
             OnCalendar = "daily";
           };
           description = lib.mdDoc ''
-            When to run the backup. See man systemd.timer for details.
+            When to run the backup. See {manpage}`systemd.timer(5)` for details.
           '';
           example = {
             OnCalendar = "00:05";
@@ -249,6 +264,7 @@ in
     example = {
       localbackup = {
         paths = [ "/home" ];
+        exclude = [ "/home/*/.cache" ];
         repository = "/mnt/backup-hdd";
         passwordFile = "/etc/nixos/secrets/restic-password";
         initialize = true;
@@ -270,20 +286,25 @@ 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);
+    assertions = mapAttrsToList (n: v: {
+      assertion = (v.repository == null) != (v.repositoryFile == null);
+      message = "services.restic.backups.${n}: exactly one of repository or repositoryFile should be set";
+    }) config.services.restic.backups;
     systemd.services =
       mapAttrs'
         (name: backup:
           let
             extraOptions = concatMapStrings (arg: " -o ${arg}") backup.extraOptions;
             resticCmd = "${backup.package}/bin/restic${extraOptions}";
+            excludeFlags = if (backup.exclude != []) then ["--exclude-file=${pkgs.writeText "exclude-patterns" (concatStringsSep "\n" backup.exclude)}"] else [];
             filesFromTmpFile = "/run/restic-backups-${name}/includes";
             backupPaths =
               if (backup.dynamicFilesFrom == null)
-              then if (backup.paths != null) then concatStringsSep " " backup.paths else ""
+              then optionalString (backup.paths != null) (concatStringsSep " " backup.paths)
               else "--files-from ${filesFromTmpFile}";
             pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [
-              (resticCmd + " forget --prune --cache-dir=%C/restic-backups-${name} " + (concatStringsSep " " backup.pruneOpts))
-              (resticCmd + " check --cache-dir=%C/restic-backups-${name} " + (concatStringsSep " " backup.checkOpts))
+              (resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts))
+              (resticCmd + " check " + (concatStringsSep " " backup.checkOpts))
             ];
             # Helper functions for rclone remotes
             rcloneRemoteName = builtins.elemAt (splitString ":" backup.repository) 1;
@@ -293,6 +314,7 @@ in
           in
           nameValuePair "restic-backups-${name}" ({
             environment = {
+              RESTIC_CACHE_DIR = "%C/restic-backups-${name}";
               RESTIC_PASSWORD_FILE = backup.passwordFile;
               RESTIC_REPOSITORY = backup.repository;
               RESTIC_REPOSITORY_FILE = backup.repositoryFile;
@@ -311,12 +333,13 @@ in
             restartIfChanged = false;
             serviceConfig = {
               Type = "oneshot";
-              ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup --cache-dir=%C/restic-backups-${name} ${concatStringsSep " " backup.extraBackupArgs} ${backupPaths}" ])
+              ExecStart = (optionals (backupPaths != "") [ "${resticCmd} backup ${concatStringsSep " " (backup.extraBackupArgs ++ excludeFlags)} ${backupPaths}" ])
                 ++ pruneCmd;
               User = backup.user;
               RuntimeDirectory = "restic-backups-${name}";
               CacheDirectory = "restic-backups-${name}";
               CacheDirectoryMode = "0700";
+              PrivateTmp = true;
             } // optionalAttrs (backup.environmentFile != null) {
               EnvironmentFile = backup.environmentFile;
             };
diff --git a/nixos/modules/services/backup/sanoid.nix b/nixos/modules/services/backup/sanoid.nix
index a51708170fb..aae77cee07d 100644
--- a/nixos/modules/services/backup/sanoid.nix
+++ b/nixos/modules/services/backup/sanoid.nix
@@ -114,6 +114,8 @@ in
   options.services.sanoid = {
     enable = mkEnableOption (lib.mdDoc "Sanoid ZFS snapshotting service");
 
+    package = lib.mkPackageOptionMD pkgs "sanoid" {};
+
     interval = mkOption {
       type = types.str;
       default = "hourly";
@@ -181,7 +183,7 @@ in
         ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets);
         ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets);
         ExecStart = lib.escapeShellArgs ([
-          "${pkgs.sanoid}/bin/sanoid"
+          "${cfg.package}/bin/sanoid"
           "--cron"
           "--configdir"
           (pkgs.writeTextDir "sanoid.conf" configFile)
diff --git a/nixos/modules/services/backup/syncoid.nix b/nixos/modules/services/backup/syncoid.nix
index 6188f109463..0f375455e7e 100644
--- a/nixos/modules/services/backup/syncoid.nix
+++ b/nixos/modules/services/backup/syncoid.nix
@@ -87,6 +87,8 @@ in
   options.services.syncoid = {
     enable = mkEnableOption (lib.mdDoc "Syncoid ZFS synchronization service");
 
+    package = lib.mkPackageOptionMD pkgs "sanoid" {};
+
     interval = mkOption {
       type = types.str;
       default = "hourly";
@@ -331,7 +333,7 @@ in
               ExecStopPost =
                 (map (buildUnallowCommand c.localSourceAllow) (localDatasetName c.source)) ++
                 (map (buildUnallowCommand c.localTargetAllow) (localDatasetName c.target));
-              ExecStart = lib.escapeShellArgs ([ "${pkgs.sanoid}/bin/syncoid" ]
+              ExecStart = lib.escapeShellArgs ([ "${cfg.package}/bin/syncoid" ]
                 ++ optionals c.useCommonArgs cfg.commonArgs
                 ++ optional c.recursive "-r"
                 ++ optionals (c.sshKey != null) [ "--sshkey" c.sshKey ]
diff --git a/nixos/modules/services/backup/zfs-replication.nix b/nixos/modules/services/backup/zfs-replication.nix
index ce914003c62..8e7059e5b59 100644
--- a/nixos/modules/services/backup/zfs-replication.nix
+++ b/nixos/modules/services/backup/zfs-replication.nix
@@ -9,7 +9,7 @@ let
 in {
   options = {
     services.zfs.autoReplication = {
-      enable = mkEnableOption (lib.mdDoc "ZFS snapshot replication.");
+      enable = mkEnableOption (lib.mdDoc "ZFS snapshot replication");
 
       followDelete = mkOption {
         description = lib.mdDoc "Remove remote snapshots that don't have a local correspondent.";
diff --git a/nixos/modules/services/blockchain/ethereum/geth.nix b/nixos/modules/services/blockchain/ethereum/geth.nix
index eca308dc366..d12516ca2f2 100644
--- a/nixos/modules/services/blockchain/ethereum/geth.nix
+++ b/nixos/modules/services/blockchain/ethereum/geth.nix
@@ -196,9 +196,9 @@ in
           --gcmode ${cfg.gcmode} \
           --port ${toString cfg.port} \
           --maxpeers ${toString cfg.maxpeers} \
-          ${if cfg.http.enable then ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}'' else ""} \
+          ${optionalString cfg.http.enable ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}''} \
           ${optionalString (cfg.http.apis != null) ''--http.api ${lib.concatStringsSep "," cfg.http.apis}''} \
-          ${if cfg.websocket.enable then ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}'' else ""} \
+          ${optionalString cfg.websocket.enable ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}''} \
           ${optionalString (cfg.websocket.apis != null) ''--ws.api ${lib.concatStringsSep "," cfg.websocket.apis}''} \
           ${optionalString cfg.metrics.enable ''--metrics --metrics.addr ${cfg.metrics.address} --metrics.port ${toString cfg.metrics.port}''} \
           --authrpc.addr ${cfg.authrpc.address} --authrpc.port ${toString cfg.authrpc.port} --authrpc.vhosts ${lib.concatStringsSep "," cfg.authrpc.vhosts} \
diff --git a/nixos/modules/services/cluster/hadoop/hbase.nix b/nixos/modules/services/cluster/hadoop/hbase.nix
index 97951ebfe33..a39da2a84ec 100644
--- a/nixos/modules/services/cluster/hadoop/hbase.nix
+++ b/nixos/modules/services/cluster/hadoop/hbase.nix
@@ -5,11 +5,95 @@ let
   cfg = config.services.hadoop;
   hadoopConf = "${import ./conf.nix { inherit cfg pkgs lib; }}/";
   mkIfNotNull = x: mkIf (x != null) x;
+  # generic hbase role options
+  hbaseRoleOption = name: extraOpts: {
+    enable = mkEnableOption (mdDoc "HBase ${name}");
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc "Open firewall ports for HBase ${name}.";
+    };
+
+    restartIfChanged = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc "Restart ${name} con config change.";
+    };
+
+    extraFlags = mkOption {
+      type = with types; listOf str;
+      default = [];
+      example = literalExpression ''[ "--backup" ]'';
+      description = mdDoc "Extra flags for the ${name} service.";
+    };
+
+    environment = mkOption {
+      type = with types; attrsOf str;
+      default = {};
+      example = literalExpression ''
+        {
+          HBASE_MASTER_OPTS = "-Dcom.sun.management.jmxremote.ssl=true";
+        }
+      '';
+      description = mdDoc "Environment variables passed to ${name}.";
+    };
+  } // extraOpts;
+  # generic hbase role configs
+  hbaseRoleConfig = name: ports: (mkIf cfg.hbase."${name}".enable {
+    services.hadoop.gatewayRole = {
+      enable = true;
+      enableHbaseCli = mkDefault true;
+    };
+
+    systemd.services."hbase-${toLower name}" = {
+      description = "HBase ${name}";
+      wantedBy = [ "multi-user.target" ];
+      path = with cfg; [ hbase.package ] ++ optional
+        (with cfg.hbase.master; enable && initHDFS) package;
+      preStart = mkIf (with cfg.hbase.master; enable && initHDFS)
+        (concatStringsSep "\n" (
+          map (x: "HADOOP_USER_NAME=hdfs hdfs --config /etc/hadoop-conf ${x}")[
+            "dfsadmin -safemode wait"
+            "dfs -mkdir -p ${cfg.hbase.rootdir}"
+            "dfs -chown hbase ${cfg.hbase.rootdir}"
+          ]
+        ));
+
+      inherit (cfg.hbase."${name}") environment;
+      script = concatStringsSep " " (
+        [
+          "hbase --config /etc/hadoop-conf/"
+          "${toLower name} start"
+        ]
+        ++ cfg.hbase."${name}".extraFlags
+        ++ map (x: "--${toLower x} ${toString cfg.hbase.${name}.${x}}")
+          (filter (x: hasAttr x cfg.hbase.${name}) ["port" "infoPort"])
+      );
+
+      serviceConfig = {
+        User = "hbase";
+        SyslogIdentifier = "hbase-${toLower name}";
+        Restart = "always";
+      };
+    };
+
+    services.hadoop.hbaseSiteInternal."hbase.rootdir" = cfg.hbase.rootdir;
+
+    networking = {
+      firewall.allowedTCPPorts = mkIf cfg.hbase."${name}".openFirewall ports;
+      hosts = mkIf (with cfg.hbase.regionServer; enable && overrideHosts) {
+        "127.0.0.2" = mkForce [ ];
+        "::1" = mkForce [ ];
+      };
+    };
+
+  });
 in
 {
   options.services.hadoop = {
 
-    gatewayRole.enableHbaseCli = mkEnableOption (lib.mdDoc "HBase CLI tools");
+    gatewayRole.enableHbaseCli = mkEnableOption (mdDoc "HBase CLI tools");
 
     hbaseSiteDefault = mkOption {
       default = {
@@ -21,7 +105,7 @@ in
         "hbase.cluster.distributed" = "true";
       };
       type = types.attrsOf types.anything;
-      description = lib.mdDoc ''
+      description = mdDoc ''
         Default options for hbase-site.xml
       '';
     };
@@ -29,8 +113,12 @@ in
       default = {};
       type = with types; attrsOf anything;
       example = literalExpression ''
+        {
+          "hbase.hregion.max.filesize" = 20*1024*1024*1024;
+          "hbase.table.normalization.enabled" = "true";
+        }
       '';
-      description = lib.mdDoc ''
+      description = mdDoc ''
         Additional options and overrides for hbase-site.xml
         <https://github.com/apache/hbase/blob/rel/2.4.11/hbase-common/src/main/resources/hbase-default.xml>
       '';
@@ -39,7 +127,7 @@ in
       default = {};
       type = with types; attrsOf anything;
       internal = true;
-      description = lib.mdDoc ''
+      description = mdDoc ''
         Internal option to add configs to hbase-site.xml based on module options
       '';
     };
@@ -50,11 +138,11 @@ in
         type = types.package;
         default = pkgs.hbase;
         defaultText = literalExpression "pkgs.hbase";
-        description = lib.mdDoc "HBase package";
+        description = mdDoc "HBase package";
       };
 
       rootdir = mkOption {
-        description = lib.mdDoc ''
+        description = mdDoc ''
           This option will set "hbase.rootdir" in hbase-site.xml and determine
           the directory shared by region servers and into which HBase persists.
           The URL should be 'fully-qualified' to include the filesystem scheme.
@@ -68,7 +156,7 @@ in
         default = "/hbase";
       };
       zookeeperQuorum = mkOption {
-        description = lib.mdDoc ''
+        description = mdDoc ''
           This option will set "hbase.zookeeper.quorum" in hbase-site.xml.
           Comma separated list of servers in the ZooKeeper ensemble.
         '';
@@ -76,107 +164,36 @@ in
         example = "zk1.internal,zk2.internal,zk3.internal";
         default = null;
       };
-      master = {
-        enable = mkEnableOption (lib.mdDoc "HBase Master");
-        initHDFS = mkEnableOption (lib.mdDoc "initialization of the hbase directory on HDFS");
-
-        openFirewall = mkOption {
-          type = types.bool;
-          default = false;
-          description = lib.mdDoc ''
-            Open firewall ports for HBase master.
-          '';
+    } // (let
+      ports = port: infoPort: {
+        port = mkOption {
+          type = types.int;
+          default = port;
+          description = mdDoc "RPC port";
         };
-      };
-      regionServer = {
-        enable = mkEnableOption (lib.mdDoc "HBase RegionServer");
-
-        overrideHosts = mkOption {
-          type = types.bool;
-          default = true;
-          description = lib.mdDoc ''
-            Remove /etc/hosts entries for "127.0.0.2" and "::1" defined in nixos/modules/config/networking.nix
-            Regionservers must be able to resolve their hostnames to their IP addresses, through PTR records
-            or /etc/hosts entries.
-
-          '';
-        };
-
-        openFirewall = mkOption {
-          type = types.bool;
-          default = false;
-          description = lib.mdDoc ''
-            Open firewall ports for HBase master.
-          '';
+        infoPort = mkOption {
+          type = types.int;
+          default = infoPort;
+          description = mdDoc "web UI port";
         };
       };
-    };
-  };
-
-  config = mkMerge [
-    (mkIf cfg.hbase.master.enable {
-      services.hadoop.gatewayRole = {
-        enable = true;
-        enableHbaseCli = mkDefault true;
-      };
-
-      systemd.services.hbase-master = {
-        description = "HBase master";
-        wantedBy = [ "multi-user.target" ];
-
-        preStart = mkIf cfg.hbase.master.initHDFS ''
-          HADOOP_USER_NAME=hdfs ${cfg.package}/bin/hdfs --config ${hadoopConf} dfsadmin -safemode wait
-          HADOOP_USER_NAME=hdfs ${cfg.package}/bin/hdfs --config ${hadoopConf} dfs -mkdir -p ${cfg.hbase.rootdir}
-          HADOOP_USER_NAME=hdfs ${cfg.package}/bin/hdfs --config ${hadoopConf} dfs -chown hbase ${cfg.hbase.rootdir}
+    in mapAttrs hbaseRoleOption {
+      master.initHDFS = mkEnableOption (mdDoc "initialization of the hbase directory on HDFS");
+      regionServer.overrideHosts = mkOption {
+        type = types.bool;
+        default = true;
+        description = mdDoc ''
+          Remove /etc/hosts entries for "127.0.0.2" and "::1" defined in nixos/modules/config/networking.nix
+          Regionservers must be able to resolve their hostnames to their IP addresses, through PTR records
+          or /etc/hosts entries.
         '';
-
-        serviceConfig = {
-          User = "hbase";
-          SyslogIdentifier = "hbase-master";
-          ExecStart = "${cfg.hbase.package}/bin/hbase --config ${hadoopConf} " +
-                      "master start";
-          Restart = "always";
-        };
-      };
-
-      services.hadoop.hbaseSiteInternal."hbase.rootdir" = cfg.hbase.rootdir;
-
-      networking.firewall.allowedTCPPorts = mkIf cfg.hbase.master.openFirewall [
-        16000 16010
-      ];
-
-    })
-
-    (mkIf cfg.hbase.regionServer.enable {
-      services.hadoop.gatewayRole = {
-        enable = true;
-        enableHbaseCli = mkDefault true;
-      };
-
-      systemd.services.hbase-regionserver = {
-        description = "HBase RegionServer";
-        wantedBy = [ "multi-user.target" ];
-        serviceConfig = {
-          User = "hbase";
-          SyslogIdentifier = "hbase-regionserver";
-          ExecStart = "${cfg.hbase.package}/bin/hbase --config /etc/hadoop-conf/ " +
-                      "regionserver start";
-          Restart = "always";
-        };
       };
+      thrift = ports 9090 9095;
+      rest = ports 8080 8085;
+    });
+  };
 
-      services.hadoop.hbaseSiteInternal."hbase.rootdir" = cfg.hbase.rootdir;
-
-      networking = {
-        firewall.allowedTCPPorts = mkIf cfg.hbase.regionServer.openFirewall [
-          16020 16030
-        ];
-        hosts = mkIf cfg.hbase.regionServer.overrideHosts {
-          "127.0.0.2" = mkForce [ ];
-          "::1" = mkForce [ ];
-        };
-      };
-    })
+  config = mkMerge ([
 
     (mkIf cfg.gatewayRole.enable {
 
@@ -192,5 +209,10 @@ in
         isSystemUser = true;
       };
     })
-  ];
+  ] ++ (mapAttrsToList hbaseRoleConfig {
+    master = [ 16000 16010 ];
+    regionServer = [ 16020 16030 ];
+    thrift = with cfg.hbase.thrift; [ port infoPort ];
+    rest = with cfg.hbase.rest; [ port infoPort ];
+  }));
 }
diff --git a/nixos/modules/services/cluster/k3s/default.nix b/nixos/modules/services/cluster/k3s/default.nix
index 693f388de14..72b2f992a33 100644
--- a/nixos/modules/services/cluster/k3s/default.nix
+++ b/nixos/modules/services/cluster/k3s/default.nix
@@ -106,6 +106,14 @@ in
       description = lib.mdDoc "Only run the server. This option only makes sense for a server.";
     };
 
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      description = lib.mdDoc ''
+        File path containing environment variables for configuring the k3s service in the format of an EnvironmentFile. See systemd.exec(5).
+      '';
+      default = null;
+    };
+
     configPath = mkOption {
       type = types.nullOr types.path;
       default = null;
@@ -139,8 +147,8 @@ in
 
     systemd.services.k3s = {
       description = "k3s service";
-      after = [ "network.service" "firewall.service" ];
-      wants = [ "network.service" "firewall.service" ];
+      after = [ "firewall.service" "network-online.target" ];
+      wants = [ "firewall.service" "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       path = optional config.boot.zfs.enabled config.boot.zfs.package;
       serviceConfig = {
@@ -154,6 +162,7 @@ in
         LimitNPROC = "infinity";
         LimitCORE = "infinity";
         TasksMax = "infinity";
+        EnvironmentFile = cfg.environmentFile;
         ExecStart = concatStringsSep " \\\n " (
           [
             "${cfg.package}/bin/k3s ${cfg.role}"
diff --git a/nixos/modules/services/cluster/kubernetes/addon-manager.nix b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
index 7aa2a8323b1..dc851688fbe 100644
--- a/nixos/modules/services/cluster/kubernetes/addon-manager.nix
+++ b/nixos/modules/services/cluster/kubernetes/addon-manager.nix
@@ -62,7 +62,7 @@ in
       '';
     };
 
-    enable = mkEnableOption (lib.mdDoc "Kubernetes addon manager.");
+    enable = mkEnableOption (lib.mdDoc "Kubernetes addon manager");
   };
 
   ###### implementation
diff --git a/nixos/modules/services/cluster/kubernetes/addons/dns.nix b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
index 3d41b5f0085..1c00329e6cc 100644
--- a/nixos/modules/services/cluster/kubernetes/addons/dns.nix
+++ b/nixos/modules/services/cluster/kubernetes/addons/dns.nix
@@ -3,7 +3,7 @@
 with lib;
 
 let
-  version = "1.7.1";
+  version = "1.10.1";
   cfg = config.services.kubernetes.addons.dns;
   ports = {
     dns = 10053;
@@ -59,9 +59,9 @@ in {
       type = types.attrs;
       default = {
         imageName = "coredns/coredns";
-        imageDigest = "sha256:4a6e0769130686518325b21b0c1d0688b54e7c79244d48e1b15634e98e40c6ef";
+        imageDigest = "sha256:a0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e";
         finalImageTag = version;
-        sha256 = "02r440xcdsgi137k5lmmvp0z5w5fmk8g9mysq5pnysq1wl8sj6mw";
+        sha256 = "0wg696920smmal7552a2zdhfncndn5kfammfa8bk8l7dz9bhk0y1";
       };
     };
 
@@ -136,6 +136,11 @@ in {
             resources = [ "nodes" ];
             verbs = [ "get" ];
           }
+          {
+            apiGroups = [ "discovery.k8s.io" ];
+            resources = [ "endpointslices" ];
+            verbs = [ "list" "watch" ];
+          }
         ];
       };
 
diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix
index 3ede1cb80e8..eebacb3f3ef 100644
--- a/nixos/modules/services/cluster/kubernetes/kubelet.nix
+++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix
@@ -63,6 +63,7 @@ in
     (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "cadvisorPort" ] "")
     (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "allowPrivileged" ] "")
     (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "networkPlugin" ] "")
+    (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "containerRuntime" ] "")
   ];
 
   ###### interface
@@ -134,19 +135,13 @@ in
       };
     };
 
-    containerRuntime = mkOption {
-      description = lib.mdDoc "Which container runtime type to use";
-      type = enum ["docker" "remote"];
-      default = "remote";
-    };
-
     containerRuntimeEndpoint = mkOption {
       description = lib.mdDoc "Endpoint at which to find the container runtime api interface/socket";
       type = str;
       default = "unix:///run/containerd/containerd.sock";
     };
 
-    enable = mkEnableOption (lib.mdDoc "Kubernetes kubelet.");
+    enable = mkEnableOption (lib.mdDoc "Kubernetes kubelet");
 
     extraOpts = mkOption {
       description = lib.mdDoc "Kubernetes kubelet extra command line options.";
@@ -331,7 +326,6 @@ in
             ${optionalString (cfg.tlsKeyFile != null)
               "--tls-private-key-file=${cfg.tlsKeyFile}"} \
             ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \
-            --container-runtime=${cfg.containerRuntime} \
             --container-runtime-endpoint=${cfg.containerRuntimeEndpoint} \
             --cgroup-driver=systemd \
             ${cfg.extraOpts}
diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix
index 26fe0f5e9e0..38682701ea1 100644
--- a/nixos/modules/services/cluster/kubernetes/pki.nix
+++ b/nixos/modules/services/cluster/kubernetes/pki.nix
@@ -270,7 +270,7 @@ in
           '';
         })]);
 
-      environment.etc.${cfg.etcClusterAdminKubeconfig}.source = mkIf (!isNull cfg.etcClusterAdminKubeconfig)
+      environment.etc.${cfg.etcClusterAdminKubeconfig}.source = mkIf (cfg.etcClusterAdminKubeconfig != null)
         clusterAdminKubeconfig;
 
       environment.systemPackages = mkIf (top.kubelet.enable || top.proxy.enable) [
diff --git a/nixos/modules/services/computing/boinc/client.nix b/nixos/modules/services/computing/boinc/client.nix
index 5fb715f4d77..1879fef9666 100644
--- a/nixos/modules/services/computing/boinc/client.nix
+++ b/nixos/modules/services/computing/boinc/client.nix
@@ -6,7 +6,7 @@ let
   cfg = config.services.boinc;
   allowRemoteGuiRpcFlag = optionalString cfg.allowRemoteGuiRpc "--allow_remote_gui_rpc";
 
-  fhsEnv = pkgs.buildFHSUserEnv {
+  fhsEnv = pkgs.buildFHSEnv {
     name = "boinc-fhs-env";
     targetPkgs = pkgs': [ cfg.package ] ++ cfg.extraEnvPackages;
     runScript = "/bin/boinc_client";
diff --git a/nixos/modules/services/computing/slurm/slurm.nix b/nixos/modules/services/computing/slurm/slurm.nix
index 0c80e79d4b7..344c43a429b 100644
--- a/nixos/modules/services/computing/slurm/slurm.nix
+++ b/nixos/modules/services/computing/slurm/slurm.nix
@@ -383,7 +383,7 @@ in
       "d /var/spool/slurmd 755 root root -"
     ];
 
-    services.openssh.forwardX11 = mkIf cfg.client.enable (mkDefault true);
+    services.openssh.settings.X11Forwarding = mkIf cfg.client.enable (mkDefault true);
 
     systemd.services.slurmctld = mkIf (cfg.server.enable) {
       path = with pkgs; [ wrappedSlurm munge coreutils ]
diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index 5666199c484..595374ea1e5 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -8,7 +8,8 @@ let
   cfg = config.services.buildbot-master;
   opt = options.services.buildbot-master;
 
-  python = cfg.package.pythonModule;
+  package = pkgs.python3.pkgs.toPythonModule cfg.package;
+  python = package.pythonModule;
 
   escapeStr = escape [ "'" ];
 
@@ -212,10 +213,10 @@ in {
 
       package = mkOption {
         type = types.package;
-        default = pkgs.python3Packages.buildbot-full;
-        defaultText = literalExpression "pkgs.python3Packages.buildbot-full";
+        default = pkgs.buildbot-full;
+        defaultText = literalExpression "pkgs.buildbot-full";
         description = lib.mdDoc "Package to use for buildbot.";
-        example = literalExpression "pkgs.python3Packages.buildbot";
+        example = literalExpression "pkgs.buildbot";
       };
 
       packages = mkOption {
@@ -255,7 +256,7 @@ in {
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       path = cfg.packages ++ cfg.pythonPackages python.pkgs;
-      environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ cfg.package ])}/${python.sitePackages}";
+      environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ package ])}/${python.sitePackages}";
 
       preStart = ''
         mkdir -vp "${cfg.buildbotDir}"
diff --git a/nixos/modules/services/continuous-integration/buildbot/worker.nix b/nixos/modules/services/continuous-integration/buildbot/worker.nix
index 52c41c4a758..7e78b8935f8 100644
--- a/nixos/modules/services/continuous-integration/buildbot/worker.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/worker.nix
@@ -8,7 +8,8 @@ let
   cfg = config.services.buildbot-worker;
   opt = options.services.buildbot-worker;
 
-  python = cfg.package.pythonModule;
+  package = pkgs.python3.pkgs.toPythonModule cfg.package;
+  python = package.pythonModule;
 
   tacFile = pkgs.writeText "aur-buildbot-worker.tac" ''
     import os
@@ -129,7 +130,7 @@ in {
 
       package = mkOption {
         type = types.package;
-        default = pkgs.python3Packages.buildbot-worker;
+        default = pkgs.buildbot-worker;
         defaultText = literalExpression "pkgs.python3Packages.buildbot-worker";
         description = lib.mdDoc "Package to use for buildbot worker.";
         example = literalExpression "pkgs.python2Packages.buildbot-worker";
@@ -168,7 +169,7 @@ in {
       after = [ "network.target" "buildbot-master.service" ];
       wantedBy = [ "multi-user.target" ];
       path = cfg.packages;
-      environment.PYTHONPATH = "${python.withPackages (p: [ cfg.package ])}/${python.sitePackages}";
+      environment.PYTHONPATH = "${python.withPackages (p: [ package ])}/${python.sitePackages}";
 
       preStart = ''
         mkdir -vp "${cfg.buildbotDir}/info"
diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix
index 24d02c931a4..67e71659d6b 100644
--- a/nixos/modules/services/continuous-integration/github-runner.nix
+++ b/nixos/modules/services/continuous-integration/github-runner.nix
@@ -20,4 +20,6 @@ in
   config = mkIf cfg.enable {
     services.github-runners.${cfg.name} = cfg;
   };
+
+  meta.maintainers = with maintainers; [ veehaitch newam ];
 }
diff --git a/nixos/modules/services/continuous-integration/github-runner/options.nix b/nixos/modules/services/continuous-integration/github-runner/options.nix
index 72ac0c12990..ce880921372 100644
--- a/nixos/modules/services/continuous-integration/github-runner/options.nix
+++ b/nixos/modules/services/continuous-integration/github-runner/options.nix
@@ -41,17 +41,42 @@ with lib;
   tokenFile = mkOption {
     type = types.path;
     description = lib.mdDoc ''
-      The full path to a file which contains either a runner registration token or a
-      (fine-grained) personal access token (PAT).
+      The full path to a file which contains either
+
+      * a fine-grained personal access token (PAT),
+      * a classic PAT
+      * or a runner registration token
+
+      Changing this option or the `tokenFile`’s content triggers a new runner registration.
+
+      We suggest using the fine-grained PATs. A runner registration token is valid
+      only for 1 hour after creation, so the next time the runner configuration changes
+      this will give you hard-to-debug HTTP 404 errors in the configure step.
+
       The file should contain exactly one line with the token without any newline.
+      (Use `echo -n '…token…' > …token file…` to make sure no newlines sneak in.)
+
+      If the file contains a PAT, the service creates a new registration token
+      on startup as needed.
       If a registration token is given, it can be used to re-register a runner of the same
-      name but is time-limited. If the file contains a PAT, the service creates a new
-      registration token on startup as needed. Make sure the PAT has a scope of
-      `admin:org` for organization-wide registrations or a scope of
-      `repo` for a single repository. Fine-grained PATs need read and write permission
-      to the "Administration" resources.
+      name but is time-limited as noted above.
+
+      For fine-grained PATs:
+
+      Give it "Read and Write access to organization/repository self hosted runners",
+      depending on whether it is organization wide or per-repository. You might have to
+      experiment a little, fine-grained PATs are a `beta` Github feature and still subject
+      to change; nonetheless they are the best option at the moment.
+
+      For classic PATs:
 
-      Changing this option or the file's content triggers a new runner registration.
+      Make sure the PAT has a scope of `admin:org` for organization-wide registrations
+      or a scope of `repo` for a single repository.
+
+      For runner registration tokens:
+
+      Nothing special needs to be done, but updating will break after one hour,
+      so these are not recommended.
     '';
     example = "/run/secrets/github-runner/nixos.token";
   };
@@ -127,10 +152,11 @@ with lib;
   serviceOverrides = mkOption {
     type = types.attrs;
     description = lib.mdDoc ''
-      Overrides for the systemd service. Can be used to adjust the sandboxing options.
+      Modify the systemd service. Can be used to, e.g., adjust the sandboxing options.
     '';
     example = {
       ProtectHome = false;
+      RestrictAddressFamilies = [ "AF_PACKET" ];
     };
     default = {};
   };
@@ -170,4 +196,16 @@ with lib;
     default = null;
     defaultText = literalExpression "username";
   };
+
+  workDir = mkOption {
+    type = with types; nullOr str;
+    description = lib.mdDoc ''
+      Working directory, available as `$GITHUB_WORKSPACE` during workflow runs
+      and used as a default for [repository checkouts](https://github.com/actions/checkout).
+      The service cleans this directory on every service start.
+
+      A value of `null` will default to the systemd `RuntimeDirectory`.
+    '';
+    default = null;
+  };
 }
diff --git a/nixos/modules/services/continuous-integration/github-runner/service.nix b/nixos/modules/services/continuous-integration/github-runner/service.nix
index cd81631582f..55df83362cb 100644
--- a/nixos/modules/services/continuous-integration/github-runner/service.nix
+++ b/nixos/modules/services/continuous-integration/github-runner/service.nix
@@ -20,6 +20,9 @@
 
 with lib;
 
+let
+  workDir = if cfg.workDir == null then runtimeDir else cfg.workDir;
+in
 {
   description = "GitHub Actions runner";
 
@@ -28,7 +31,7 @@ with lib;
   after = [ "network.target" "network-online.target" ];
 
   environment = {
-    HOME = runtimeDir;
+    HOME = workDir;
     RUNNER_ROOT = stateDir;
   } // cfg.extraEnvironment;
 
@@ -42,216 +45,223 @@ with lib;
     config.nix.package
   ] ++ cfg.extraPackages;
 
-  serviceConfig = rec {
-    ExecStart = "${cfg.package}/bin/Runner.Listener run --startuptype service";
+  serviceConfig = mkMerge [
+    {
+      ExecStart = "${cfg.package}/bin/Runner.Listener run --startuptype service";
 
-    # Does the following, sequentially:
-    # - If the module configuration or the token has changed, purge the state directory,
-    #   and create the current and the new token file with the contents of the configured
-    #   token. While both files have the same content, only the later is accessible by
-    #   the service user.
-    # - Configure the runner using the new token file. When finished, delete it.
-    # - Set up the directory structure by creating the necessary symlinks.
-    ExecStartPre =
-      let
-        # Wrapper script which expects the full path of the state, runtime and logs
-        # directory as arguments. Overrides the respective systemd variables to provide
-        # unambiguous directory names. This becomes relevant, for example, if the
-        # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
-        # to contain more than one directory. This causes systemd to set the respective
-        # environment variables with the path of all of the given directories, separated
-        # by a colon.
-        writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
-          set -euo pipefail
+      # Does the following, sequentially:
+      # - If the module configuration or the token has changed, purge the state directory,
+      #   and create the current and the new token file with the contents of the configured
+      #   token. While both files have the same content, only the later is accessible by
+      #   the service user.
+      # - Configure the runner using the new token file. When finished, delete it.
+      # - Set up the directory structure by creating the necessary symlinks.
+      ExecStartPre =
+        let
+          # Wrapper script which expects the full path of the state, working and logs
+          # directory as arguments. Overrides the respective systemd variables to provide
+          # unambiguous directory names. This becomes relevant, for example, if the
+          # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory=
+          # to contain more than one directory. This causes systemd to set the respective
+          # environment variables with the path of all of the given directories, separated
+          # by a colon.
+          writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" ''
+            set -euo pipefail
 
-          STATE_DIRECTORY="$1"
-          RUNTIME_DIRECTORY="$2"
-          LOGS_DIRECTORY="$3"
+            STATE_DIRECTORY="$1"
+            WORK_DIRECTORY="$2"
+            LOGS_DIRECTORY="$3"
 
-          ${lines}
-        '';
-        runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" "ephemeral" ] cfg;
-        newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
-        currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
-        newConfigTokenPath= "$STATE_DIRECTORY/.new-token";
-        currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}";
+            ${lines}
+          '';
+          runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" "ephemeral" "workDir" ] cfg;
+          newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig);
+          currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json";
+          newConfigTokenPath = "$STATE_DIRECTORY/.new-token";
+          currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}";
 
-        runnerCredFiles = [
-          ".credentials"
-          ".credentials_rsaparams"
-          ".runner"
-        ];
-        unconfigureRunner = writeScript "unconfigure" ''
-          copy_tokens() {
-            # Copy the configured token file to the state dir and allow the service user to read the file
-            install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}"
-            # Also copy current file to allow for a diff on the next start
-            install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}"
-          }
-          clean_state() {
-            find "$STATE_DIRECTORY/" -mindepth 1 -delete
-            copy_tokens
-          }
-          diff_config() {
-            changed=0
-            # Check for module config changes
-            [[ -f "${currentConfigPath}" ]] \
-              && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \
-              || changed=1
-            # Also check the content of the token file
-            [[ -f "${currentConfigTokenPath}" ]] \
-              && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \
-              || changed=1
-            # If the config has changed, remove old state and copy tokens
-            if [[ "$changed" -eq 1 ]]; then
-              echo "Config has changed, removing old runner state."
-              echo "The old runner will still appear in the GitHub Actions UI." \
-                   "You have to remove it manually."
+          runnerCredFiles = [
+            ".credentials"
+            ".credentials_rsaparams"
+            ".runner"
+          ];
+          unconfigureRunner = writeScript "unconfigure" ''
+            copy_tokens() {
+              # Copy the configured token file to the state dir and allow the service user to read the file
+              install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}"
+              # Also copy current file to allow for a diff on the next start
+              install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}"
+            }
+            clean_state() {
+              find "$STATE_DIRECTORY/" -mindepth 1 -delete
+              copy_tokens
+            }
+            diff_config() {
+              changed=0
+              # Check for module config changes
+              [[ -f "${currentConfigPath}" ]] \
+                && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \
+                || changed=1
+              # Also check the content of the token file
+              [[ -f "${currentConfigTokenPath}" ]] \
+                && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \
+                || changed=1
+              # If the config has changed, remove old state and copy tokens
+              if [[ "$changed" -eq 1 ]]; then
+                echo "Config has changed, removing old runner state."
+                echo "The old runner will still appear in the GitHub Actions UI." \
+                     "You have to remove it manually."
+                clean_state
+              fi
+            }
+            if [[ "${optionalString cfg.ephemeral "1"}" ]]; then
+              # In ephemeral mode, we always want to start with a clean state
               clean_state
-            fi
-          }
-          if [[ "${optionalString cfg.ephemeral "1"}" ]]; then
-            # In ephemeral mode, we always want to start with a clean state
-            clean_state
-          elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then
-            # There are state files from a previous run; diff them to decide if we need a new registration
-            diff_config
-          else
-            # The state directory is entirely empty which indicates a first start
-            copy_tokens
-          fi        '';
-        configureRunner = writeScript "configure" ''
-          if [[ -e "${newConfigTokenPath}" ]]; then
-            echo "Configuring GitHub Actions Runner"
-            args=(
-              --unattended
-              --disableupdate
-              --work "$RUNTIME_DIRECTORY"
-              --url ${escapeShellArg cfg.url}
-              --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)}
-              --name ${escapeShellArg cfg.name}
-              ${optionalString cfg.replace "--replace"}
-              ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
-              ${optionalString cfg.ephemeral "--ephemeral"}
-            )
-            # If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option,
-            # if it is not a PAT, we assume it contains a registration token and use the --token option
-            token=$(<"${newConfigTokenPath}")
-            if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then
-              args+=(--pat "$token")
+            elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then
+              # There are state files from a previous run; diff them to decide if we need a new registration
+              diff_config
             else
-              args+=(--token "$token")
+              # The state directory is entirely empty which indicates a first start
+              copy_tokens
             fi
-            ${cfg.package}/bin/config.sh "''${args[@]}"
-            # Move the automatically created _diag dir to the logs dir
-            mkdir -p  "$STATE_DIRECTORY/_diag"
-            cp    -r  "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
-            rm    -rf "$STATE_DIRECTORY/_diag/"
-            # Cleanup token from config
-            rm "${newConfigTokenPath}"
-            # Symlink to new config
-            ln -s '${newConfigPath}' "${currentConfigPath}"
-          fi
-        '';
-        setupRuntimeDir = writeScript "setup-runtime-dirs" ''
-          # Link _diag dir
-          ln -s "$LOGS_DIRECTORY" "$RUNTIME_DIRECTORY/_diag"
+            # Always clean workDir
+            find -H "$WORK_DIRECTORY" -mindepth 1 -delete
+          '';
+          configureRunner = writeScript "configure" ''
+            if [[ -e "${newConfigTokenPath}" ]]; then
+              echo "Configuring GitHub Actions Runner"
+              args=(
+                --unattended
+                --disableupdate
+                --work "$WORK_DIRECTORY"
+                --url ${escapeShellArg cfg.url}
+                --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)}
+                --name ${escapeShellArg cfg.name}
+                ${optionalString cfg.replace "--replace"}
+                ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"}
+                ${optionalString cfg.ephemeral "--ephemeral"}
+              )
+              # If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option,
+              # if it is not a PAT, we assume it contains a registration token and use the --token option
+              token=$(<"${newConfigTokenPath}")
+              if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then
+                args+=(--pat "$token")
+              else
+                args+=(--token "$token")
+              fi
+              ${cfg.package}/bin/Runner.Listener configure "''${args[@]}"
+              # Move the automatically created _diag dir to the logs dir
+              mkdir -p  "$STATE_DIRECTORY/_diag"
+              cp    -r  "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/"
+              rm    -rf "$STATE_DIRECTORY/_diag/"
+              # Cleanup token from config
+              rm "${newConfigTokenPath}"
+              # Symlink to new config
+              ln -s '${newConfigPath}' "${currentConfigPath}"
+            fi
+          '';
+          setupWorkDir = writeScript "setup-work-dirs" ''
+            # Link _diag dir
+            ln -s "$LOGS_DIRECTORY" "$WORK_DIRECTORY/_diag"
 
-          # Link the runner credentials to the runtime dir
-          ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$RUNTIME_DIRECTORY/"
-        '';
-      in
-        map (x: "${x} ${escapeShellArgs [ stateDir runtimeDir logsDir ]}") [
+            # Link the runner credentials to the work dir
+            ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$WORK_DIRECTORY/"
+          '';
+        in
+        map (x: "${x} ${escapeShellArgs [ stateDir workDir logsDir ]}") [
           "+${unconfigureRunner}" # runs as root
           configureRunner
-          setupRuntimeDir
+          setupWorkDir
         ];
 
-    # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner)
-    # to trigger a fresh registration.
-    Restart = if cfg.ephemeral then "on-success" else "no";
-    # If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service:
-    # https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146
-    RestartForceExitStatus = [ 2 ];
+      # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner)
+      # to trigger a fresh registration.
+      Restart = if cfg.ephemeral then "on-success" else "no";
+      # If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service:
+      # https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146
+      RestartForceExitStatus = [ 2 ];
+
+      # Contains _diag
+      LogsDirectory = [ systemdDir ];
+      # Default RUNNER_ROOT which contains ephemeral Runner data
+      RuntimeDirectory = [ systemdDir ];
+      # Home of persistent runner data, e.g., credentials
+      StateDirectory = [ systemdDir ];
+      StateDirectoryMode = "0700";
+      WorkingDirectory = workDir;
 
-    # Contains _diag
-    LogsDirectory = [ systemdDir ];
-    # Default RUNNER_ROOT which contains ephemeral Runner data
-    RuntimeDirectory = [ systemdDir ];
-    # Home of persistent runner data, e.g., credentials
-    StateDirectory = [ systemdDir ];
-    StateDirectoryMode = "0700";
-    WorkingDirectory = runtimeDir;
+      InaccessiblePaths = [
+        # Token file path given in the configuration, if visible to the service
+        "-${cfg.tokenFile}"
+        # Token file in the state directory
+        "${stateDir}/${currentConfigTokenFilename}"
+      ];
 
-    InaccessiblePaths = [
-      # Token file path given in the configuration, if visible to the service
-      "-${cfg.tokenFile}"
-      # Token file in the state directory
-      "${stateDir}/${currentConfigTokenFilename}"
-    ];
+      KillSignal = "SIGINT";
 
-    KillSignal = "SIGINT";
+      # Hardening (may overlap with DynamicUser=)
+      # The following options are only for optimizing:
+      # systemd-analyze security github-runner
+      AmbientCapabilities = mkBefore [ "" ];
+      CapabilityBoundingSet = mkBefore [ "" ];
+      # ProtectClock= adds DeviceAllow=char-rtc r
+      DeviceAllow = mkBefore [ "" ];
+      NoNewPrivileges = mkDefault true;
+      PrivateDevices = mkDefault true;
+      PrivateMounts = mkDefault true;
+      PrivateTmp = mkDefault true;
+      PrivateUsers = mkDefault true;
+      ProtectClock = mkDefault true;
+      ProtectControlGroups = mkDefault true;
+      ProtectHome = mkDefault true;
+      ProtectHostname = mkDefault true;
+      ProtectKernelLogs = mkDefault true;
+      ProtectKernelModules = mkDefault true;
+      ProtectKernelTunables = mkDefault true;
+      ProtectSystem = mkDefault "strict";
+      RemoveIPC = mkDefault true;
+      RestrictNamespaces = mkDefault true;
+      RestrictRealtime = mkDefault true;
+      RestrictSUIDSGID = mkDefault true;
+      UMask = mkDefault "0066";
+      ProtectProc = mkDefault "invisible";
+      SystemCallFilter = mkBefore [
+        "~@clock"
+        "~@cpu-emulation"
+        "~@module"
+        "~@mount"
+        "~@obsolete"
+        "~@raw-io"
+        "~@reboot"
+        "~capset"
+        "~setdomainname"
+        "~sethostname"
+      ];
+      RestrictAddressFamilies = mkBefore [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ];
 
-    # Hardening (may overlap with DynamicUser=)
-    # The following options are only for optimizing:
-    # systemd-analyze security github-runner
-    AmbientCapabilities = "";
-    CapabilityBoundingSet = "";
-    # ProtectClock= adds DeviceAllow=char-rtc r
-    DeviceAllow = "";
-    NoNewPrivileges = true;
-    PrivateDevices = true;
-    PrivateMounts = true;
-    PrivateTmp = true;
-    PrivateUsers = true;
-    ProtectClock = true;
-    ProtectControlGroups = true;
-    ProtectHome = true;
-    ProtectHostname = true;
-    ProtectKernelLogs = true;
-    ProtectKernelModules = true;
-    ProtectKernelTunables = true;
-    ProtectSystem = "strict";
-    RemoveIPC = true;
-    RestrictNamespaces = true;
-    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" ];
+      BindPaths = lib.optionals (cfg.workDir != null) [ cfg.workDir ];
 
-    # Needs network access
-    PrivateNetwork = false;
-    # Cannot be true due to Node
-    MemoryDenyWriteExecute = false;
+      # Needs network access
+      PrivateNetwork = mkDefault false;
+      # Cannot be true due to Node
+      MemoryDenyWriteExecute = mkDefault 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;
+      # 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 = mkDefault "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 = mkDefault false;
 
-    # Note that this has some interactions with the User setting; so you may
-    # want to consult the systemd docs if using both.
-    DynamicUser = true;
-  } // (
-    lib.optionalAttrs (cfg.user != null) { User = cfg.user; }
-  ) // cfg.serviceOverrides;
+      # Note that this has some interactions with the User setting; so you may
+      # want to consult the systemd docs if using both.
+      DynamicUser = mkDefault true;
+    }
+    (mkIf (cfg.user != null) { User = cfg.user; })
+    cfg.serviceOverrides
+  ];
 }
diff --git a/nixos/modules/services/continuous-integration/github-runners.nix b/nixos/modules/services/continuous-integration/github-runners.nix
index 78b57f9c7a2..66ace9580ec 100644
--- a/nixos/modules/services/continuous-integration/github-runners.nix
+++ b/nixos/modules/services/continuous-integration/github-runners.nix
@@ -53,4 +53,6 @@ in
         }))
     );
   };
+
+  meta.maintainers = with maintainers; [ veehaitch newam ];
 }
diff --git a/nixos/modules/services/continuous-integration/gitlab-runner.nix b/nixos/modules/services/continuous-integration/gitlab-runner.nix
index 7b1c4da8626..53f39f40daa 100644
--- a/nixos/modules/services/continuous-integration/gitlab-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitlab-runner.nix
@@ -4,24 +4,41 @@ with lib;
 let
   cfg = config.services.gitlab-runner;
   hasDocker = config.virtualisation.docker.enable;
+
+  /* The whole logic of this module is to diff the hashes of the desired vs existing runners
+  The hash is recorded in the runner's name because we can't do better yet
+  See https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29350 for more details
+  */
+  genRunnerName = name: service: let
+      hash = substring 0 12 (hashString "md5" (unsafeDiscardStringContext (toJSON service)));
+    in if service ? description && service.description != null
+    then "${hash} ${service.description}"
+    else "${name}_${config.networking.hostName}_${hash}";
+
   hashedServices = mapAttrs'
-    (name: service: nameValuePair
-      "${name}_${config.networking.hostName}_${
-        substring 0 12
-        (hashString "md5" (unsafeDiscardStringContext (toJSON service)))}"
-      service)
-    cfg.services;
-  configPath = "$HOME/.gitlab-runner/config.toml";
-  configureScript = pkgs.writeShellScriptBin "gitlab-runner-configure" (
-    if (cfg.configFile != null) then ''
-      mkdir -p $(dirname ${configPath})
+    (name: service: nameValuePair (genRunnerName name service) service) cfg.services;
+  configPath = ''"$HOME"/.gitlab-runner/config.toml'';
+  configureScript = pkgs.writeShellApplication {
+    name = "gitlab-runner-configure";
+    runtimeInputs = with pkgs; [
+        bash
+        gawk
+        jq
+        moreutils
+        remarshal
+        util-linux
+        cfg.package
+        perl
+        python3
+    ];
+    text = if (cfg.configFile != null) then ''
       cp ${cfg.configFile} ${configPath}
       # make config file readable by service
-      chown -R --reference=$HOME $(dirname ${configPath})
+      chown -R --reference="$HOME" "$(dirname ${configPath})"
     '' else ''
       export CONFIG_FILE=${configPath}
 
-      mkdir -p $(dirname ${configPath})
+      mkdir -p "$(dirname ${configPath})"
       touch ${configPath}
 
       # update global options
@@ -34,22 +51,43 @@ let
       # remove no longer existing services
       gitlab-runner verify --delete
 
-      # current and desired state
-      NEEDED_SERVICES=$(echo ${concatStringsSep " " (attrNames hashedServices)} | tr " " "\n")
-      REGISTERED_SERVICES=$(gitlab-runner list 2>&1 | grep 'Executor' | awk '{ print $1 }')
+      ${toShellVar "NEEDED_SERVICES" (lib.mapAttrs (name: value: 1) hashedServices)}
+
+      declare -A REGISTERED_SERVICES
+
+      while IFS="," read -r name token;
+      do
+        REGISTERED_SERVICES["$name"]="$token"
+      done < <(gitlab-runner --log-format json list 2>&1 | grep Token  | jq -r '.msg +"," + .Token')
+
+      echo "NEEDED_SERVICES: " "''${!NEEDED_SERVICES[@]}"
+      echo "REGISTERED_SERVICES:" "''${!REGISTERED_SERVICES[@]}"
 
       # difference between current and desired state
-      NEW_SERVICES=$(grep -vxF -f <(echo "$REGISTERED_SERVICES") <(echo "$NEEDED_SERVICES") || true)
-      OLD_SERVICES=$(grep -vxF -f <(echo "$NEEDED_SERVICES") <(echo "$REGISTERED_SERVICES") || true)
+      declare -A NEW_SERVICES
+      for name in "''${!NEEDED_SERVICES[@]}"; do
+        if [ ! -v 'REGISTERED_SERVICES[$name]' ]; then
+          NEW_SERVICES[$name]=1
+        fi
+      done
+
+      declare -A OLD_SERVICES
+      # shellcheck disable=SC2034
+      for name in "''${!REGISTERED_SERVICES[@]}"; do
+        if [ ! -v 'NEEDED_SERVICES[$name]' ]; then
+          OLD_SERVICES[$name]=1
+        fi
+      done
 
       # register new services
       ${concatStringsSep "\n" (mapAttrsToList (name: service: ''
-        if echo "$NEW_SERVICES" | grep -xq "${name}"; then
+        # TODO so here we should mention NEW_SERVICES
+        if [ -v 'NEW_SERVICES["${name}"]' ] ; then
           bash -c ${escapeShellArg (concatStringsSep " \\\n " ([
             "set -a && source ${service.registrationConfigFile} &&"
             "gitlab-runner register"
             "--non-interactive"
-            (if service.description != null then "--description \"${service.description}\"" else "--name '${name}'")
+            "--name '${name}'"
             "--executor ${service.executor}"
             "--limit ${toString service.limit}"
             "--request-concurrency ${toString service.requestConcurrency}"
@@ -92,22 +130,26 @@ let
         fi
       '') hashedServices)}
 
+      # check key is in array https://stackoverflow.com/questions/30353951/how-to-check-if-dictionary-contains-a-key-in-bash
+
+      echo "NEW_SERVICES: ''${NEW_SERVICES[*]}"
+      echo "OLD_SERVICES: ''${OLD_SERVICES[*]}"
       # unregister old services
-      for NAME in $(echo "$OLD_SERVICES")
+      for NAME in "''${!OLD_SERVICES[@]}"
       do
-        [ ! -z "$NAME" ] && gitlab-runner unregister \
+        [ -n "$NAME" ] && gitlab-runner unregister \
           --name "$NAME" && sleep 1
       done
 
       # make config file readable by service
-      chown -R --reference=$HOME $(dirname ${configPath})
-    '');
+      chown -R --reference="$HOME" "$(dirname ${configPath})"
+    '';
+  };
   startScript = pkgs.writeShellScriptBin "gitlab-runner-start" ''
     export CONFIG_FILE=${configPath}
     exec gitlab-runner run --working-directory $HOME
   '';
-in
-{
+in {
   options.services.gitlab-runner = {
     enable = mkEnableOption (lib.mdDoc "Gitlab Runner");
     configFile = mkOption {
@@ -492,9 +534,9 @@ in
     };
   };
   config = mkIf cfg.enable {
-    warnings = (mapAttrsToList
+    warnings = mapAttrsToList
       (n: v: "services.gitlab-runner.services.${n}.`registrationConfigFile` points to a file in Nix Store. You should use quoted absolute path to prevent this.")
-      (filterAttrs (n: v: isStorePath v.registrationConfigFile) cfg.services));
+      (filterAttrs (n: v: isStorePath v.registrationConfigFile) cfg.services);
 
     environment.systemPackages = [ cfg.package ];
     systemd.services.gitlab-runner = {
@@ -528,14 +570,14 @@ in
         ExecStartPre = "!${configureScript}/bin/gitlab-runner-configure";
         ExecStart = "${startScript}/bin/gitlab-runner-start";
         ExecReload = "!${configureScript}/bin/gitlab-runner-configure";
-      } // optionalAttrs (cfg.gracefulTermination) {
+      } // optionalAttrs cfg.gracefulTermination {
         TimeoutStopSec = "${cfg.gracefulTimeout}";
         KillSignal = "SIGQUIT";
         KillMode = "process";
       };
     };
     # Enable periodic clear-docker-cache script
-    systemd.services.gitlab-runner-clear-docker-cache = {
+    systemd.services.gitlab-runner-clear-docker-cache = mkIf (cfg.clear-docker-cache.enable && (any (s: s.executor == "docker") (attrValues cfg.services))) {
       description = "Prune gitlab-runner docker resources";
       restartIfChanged = false;
       unitConfig.X-StopOnRemoval = false;
@@ -548,7 +590,7 @@ in
         ${pkgs.gitlab-runner}/bin/clear-docker-cache ${toString cfg.clear-docker-cache.flags}
       '';
 
-      startAt = optional cfg.clear-docker-cache.enable cfg.clear-docker-cache.dates;
+      startAt = cfg.clear-docker-cache.dates;
     };
     # Enable docker if `docker` executor is used in any service
     virtualisation.docker.enable = mkIf (
diff --git a/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
index ef1933e1228..e28493ce776 100644
--- a/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
+++ b/nixos/modules/services/continuous-integration/hercules-ci-agent/default.nix
@@ -35,6 +35,9 @@ in
         ExecStartPre = testCommand;
         Restart = "on-failure";
         RestartSec = 120;
+
+        LimitSTACK = 256 * 1024 * 1024;
+        OOMPolicy = "continue";
       };
     };
 
diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix
index 564bcd37dec..83078706fca 100644
--- a/nixos/modules/services/continuous-integration/hydra/default.nix
+++ b/nixos/modules/services/continuous-integration/hydra/default.nix
@@ -398,7 +398,7 @@ in
     systemd.services.hydra-evaluator =
       { wantedBy = [ "multi-user.target" ];
         requires = [ "hydra-init.service" ];
-        after = [ "hydra-init.service" "network.target" ];
+        after = [ "hydra-init.service" "network.target" "network-online.target" ];
         path = with pkgs; [ hydra-package nettools jq ];
         restartTriggers = [ hydraConf ];
         environment = env // {
diff --git a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
index 3a1c6c1a371..d6a8c2a3f7c 100644
--- a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
+++ b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix
@@ -242,7 +242,7 @@ in {
                 jobdir="${jenkinsCfg.home}/$jenkinsjobname"
                 rm -rf "$jobdir"
             done
-          '' + (if cfg.accessUser != "" then reloadScript else "");
+          '' + (optionalString (cfg.accessUser != "") reloadScript);
       serviceConfig = {
         Type = "oneshot";
         User = jenkinsCfg.user;
diff --git a/nixos/modules/services/continuous-integration/woodpecker/agents.nix b/nixos/modules/services/continuous-integration/woodpecker/agents.nix
new file mode 100644
index 00000000000..caf6c850934
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/woodpecker/agents.nix
@@ -0,0 +1,144 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.woodpecker-agents;
+
+  agentModule = lib.types.submodule {
+    options = {
+      enable = lib.mkEnableOption (lib.mdDoc "this Woodpecker-Agent. Agents execute tasks generated by a Server, every install will need one server and at least one agent");
+
+      package = lib.mkPackageOptionMD pkgs "woodpecker-agent" { };
+
+      environment = lib.mkOption {
+        default = { };
+        type = lib.types.attrsOf lib.types.str;
+        example = lib.literalExpression ''
+          {
+            WOODPECKER_SERVER = "localhost:9000";
+            WOODPECKER_BACKEND = "docker";
+            DOCKER_HOST = "unix:///run/podman/podman.sock";
+          }
+        '';
+        description = lib.mdDoc "woodpecker-agent config envrionment variables, for other options read the [documentation](https://woodpecker-ci.org/docs/administration/agent-config)";
+      };
+
+      extraGroups = lib.mkOption {
+        type = lib.types.listOf lib.types.str;
+        default = [ ];
+        example = [ "podman" ];
+        description = lib.mdDoc ''
+          Additional groups for the systemd service.
+        '';
+      };
+
+      environmentFile = lib.mkOption {
+        type = lib.types.listOf lib.types.path;
+        default = [ ];
+        example = [ "/var/secrets/woodpecker-agent.env" ];
+        description = lib.mdDoc ''
+          File to load environment variables
+          from. This is helpful for specifying secrets.
+          Example content of environmentFile:
+          ```
+          WOODPECKER_AGENT_SECRET=your-shared-secret-goes-here
+          ```
+        '';
+      };
+    };
+  };
+
+  mkAgentService = name: agentCfg: {
+    name = "woodpecker-agent-${name}";
+    value = {
+      description = "Woodpecker-Agent Service - ${name}";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+      serviceConfig = {
+        DynamicUser = true;
+        SupplementaryGroups = agentCfg.extraGroups;
+        EnvironmentFile = agentCfg.environmentFile;
+        ExecStart = lib.getExe agentCfg.package;
+        Restart = "on-failure";
+        RestartSec = 15;
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "~@clock @privileged @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap";
+        BindReadOnlyPaths = [
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/ssl/certs"
+          "-/etc/static/ssl/certs"
+          "-/etc/hosts"
+          "-/etc/localtime"
+        ];
+      };
+      inherit (agentCfg) environment;
+    };
+  };
+in
+{
+  meta.maintainers = with lib.maintainers; [ janik ambroisie ];
+
+  options = {
+    services.woodpecker-agents = {
+      agents = lib.mkOption {
+        default = { };
+        type = lib.types.attrsOf agentModule;
+        example = {
+          docker = {
+            environment = {
+              WOODPECKER_SERVER = "localhost:9000";
+              WOODPECKER_BACKEND = "docker";
+              DOCKER_HOST = "unix:///run/podman/podman.sock";
+            };
+
+            extraGroups = [ "docker" ];
+
+            environmentFile = "/run/secrets/woodpecker/agent-secret.txt";
+          };
+
+          exec = {
+            environment = {
+              WOODPECKER_SERVER = "localhost:9000";
+              WOODPECKER_BACKEND = "exec";
+            };
+
+            environmentFile = "/run/secrets/woodpecker/agent-secret.txt";
+          };
+        };
+        description = lib.mdDoc "woodpecker-agents configurations";
+      };
+    };
+  };
+
+  config = {
+    systemd.services =
+      let
+        mkServices = lib.mapAttrs' mkAgentService;
+        enabledAgents = lib.filterAttrs (_: agent: agent.enable) cfg.agents;
+      in
+      mkServices enabledAgents;
+  };
+}
diff --git a/nixos/modules/services/continuous-integration/woodpecker/server.nix b/nixos/modules/services/continuous-integration/woodpecker/server.nix
new file mode 100644
index 00000000000..be7786da850
--- /dev/null
+++ b/nixos/modules/services/continuous-integration/woodpecker/server.nix
@@ -0,0 +1,98 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.woodpecker-server;
+in
+{
+  meta.maintainers = with lib.maintainers; [ janik ambroisie ];
+
+
+  options = {
+    services.woodpecker-server = {
+      enable = lib.mkEnableOption (lib.mdDoc "the Woodpecker-Server, a CI/CD application for automatic builds, deployments and tests");
+      package = lib.mkPackageOptionMD pkgs "woodpecker-server" { };
+      environment = lib.mkOption {
+        default = { };
+        type = lib.types.attrsOf lib.types.str;
+        example = lib.literalExpression
+          ''
+            {
+              WOODPECKER_HOST = "https://woodpecker.example.com";
+              WOODPECKER_OPEN = "true";
+              WOODPECKER_GITEA = "true";
+              WOODPECKER_GITEA_CLIENT = "ffffffff-ffff-ffff-ffff-ffffffffffff";
+              WOODPECKER_GITEA_URL = "https://git.example.com";
+            }
+          '';
+        description = lib.mdDoc "woodpecker-server config envrionment variables, for other options read the [documentation](https://woodpecker-ci.org/docs/administration/server-config)";
+      };
+      environmentFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/root/woodpecker-server.env";
+        description = lib.mdDoc ''
+          File to load environment variables
+          from. This is helpful for specifying secrets.
+          Example content of environmentFile:
+          ```
+          WOODPECKER_AGENT_SECRET=your-shared-secret-goes-here
+          WOODPECKER_GITEA_SECRET=gto_**************************************
+          ```
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services = {
+      woodpecker-server = {
+        description = "Woodpecker-Server Service";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network-online.target" ];
+        wants = [ "network-online.target" ];
+        serviceConfig = {
+          DynamicUser = true;
+          WorkingDirectory = "%S/woodpecker-server";
+          StateDirectory = "woodpecker-server";
+          StateDirectoryMode = "0700";
+          UMask = "0007";
+          ConfigurationDirectory = "woodpecker-server";
+          EnvironmentFile = lib.optional (cfg.environmentFile != null) cfg.environmentFile;
+          ExecStart = "${cfg.package}/bin/woodpecker-server";
+          Restart = "on-failure";
+          RestartSec = 15;
+          CapabilityBoundingSet = "";
+          # Security
+          NoNewPrivileges = true;
+          # Sandboxing
+          ProtectSystem = "strict";
+          ProtectHome = true;
+          PrivateTmp = true;
+          PrivateDevices = true;
+          PrivateUsers = true;
+          ProtectHostname = true;
+          ProtectClock = true;
+          ProtectKernelTunables = true;
+          ProtectKernelModules = true;
+          ProtectKernelLogs = true;
+          ProtectControlGroups = true;
+          RestrictAddressFamilies = [ "AF_UNIX AF_INET AF_INET6" ];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          PrivateMounts = true;
+          # System Call Filtering
+          SystemCallArchitectures = "native";
+          SystemCallFilter = "~@clock @privileged @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap";
+        };
+        inherit (cfg) environment;
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/databases/clickhouse.nix b/nixos/modules/services/databases/clickhouse.nix
index 04dd20b5f14..1f4a39765cd 100644
--- a/nixos/modules/services/databases/clickhouse.nix
+++ b/nixos/modules/services/databases/clickhouse.nix
@@ -54,7 +54,7 @@ with lib;
         AmbientCapabilities = "CAP_SYS_NICE";
         StateDirectory = "clickhouse";
         LogsDirectory = "clickhouse";
-        ExecStart = "${cfg.package}/bin/clickhouse-server --config-file=${cfg.package}/etc/clickhouse-server/config.xml";
+        ExecStart = "${cfg.package}/bin/clickhouse-server --config-file=/etc/clickhouse-server/config.xml";
       };
     };
 
diff --git a/nixos/modules/services/databases/dgraph.nix b/nixos/modules/services/databases/dgraph.nix
index 5726851a43f..7f005a9971a 100644
--- a/nixos/modules/services/databases/dgraph.nix
+++ b/nixos/modules/services/databases/dgraph.nix
@@ -12,7 +12,7 @@ let
   ''
     mkdir -p $out/bin
     makeWrapper ${cfg.package}/bin/dgraph $out/bin/dgraph \
-      --set PATH '${lib.makeBinPath [ pkgs.nodejs ]}:$PATH' \
+      --prefix PATH : "${lib.makeBinPath [ pkgs.nodejs ]}" \
   '';
   securityOptions = {
       NoNewPrivileges = true;
@@ -55,7 +55,7 @@ in
     services.dgraph = {
       enable = mkEnableOption (lib.mdDoc "Dgraph native GraphQL database with a graph backend");
 
-      package = lib.mkPackageOption pkgs "dgraph" { };
+      package = lib.mkPackageOptionMD pkgs "dgraph" { };
 
       settings = mkOption {
         type = settingsFormat.type;
diff --git a/nixos/modules/services/databases/foundationdb.md b/nixos/modules/services/databases/foundationdb.md
new file mode 100644
index 00000000000..f852c6888d8
--- /dev/null
+++ b/nixos/modules/services/databases/foundationdb.md
@@ -0,0 +1,309 @@
+# FoundationDB {#module-services-foundationdb}
+
+*Source:* {file}`modules/services/databases/foundationdb.nix`
+
+*Upstream documentation:* <https://apple.github.io/foundationdb/>
+
+*Maintainer:* Austin Seipp
+
+*Available version(s):* 5.1.x, 5.2.x, 6.0.x
+
+FoundationDB (or "FDB") is an open source, distributed, transactional
+key-value store.
+
+## Configuring and basic setup {#module-services-foundationdb-configuring}
+
+To enable FoundationDB, add the following to your
+{file}`configuration.nix`:
+```
+services.foundationdb.enable = true;
+services.foundationdb.package = pkgs.foundationdb52; # FoundationDB 5.2.x
+```
+
+The {option}`services.foundationdb.package` option is required, and
+must always be specified. Due to the fact FoundationDB network protocols and
+on-disk storage formats may change between (major) versions, and upgrades
+must be explicitly handled by the user, you must always manually specify
+this yourself so that the NixOS module will use the proper version. Note
+that minor, bugfix releases are always compatible.
+
+After running {command}`nixos-rebuild`, you can verify whether
+FoundationDB is running by executing {command}`fdbcli` (which is
+added to {option}`environment.systemPackages`):
+```ShellSession
+$ sudo -u foundationdb fdbcli
+Using cluster file `/etc/foundationdb/fdb.cluster'.
+
+The database is available.
+
+Welcome to the fdbcli. For help, type `help'.
+fdb> status
+
+Using cluster file `/etc/foundationdb/fdb.cluster'.
+
+Configuration:
+  Redundancy mode        - single
+  Storage engine         - memory
+  Coordinators           - 1
+
+Cluster:
+  FoundationDB processes - 1
+  Machines               - 1
+  Memory availability    - 5.4 GB per process on machine with least available
+  Fault Tolerance        - 0 machines
+  Server time            - 04/20/18 15:21:14
+
+...
+
+fdb>
+```
+
+You can also write programs using the available client libraries. For
+example, the following Python program can be run in order to grab the
+cluster status, as a quick example. (This example uses
+{command}`nix-shell` shebang support to automatically supply the
+necessary Python modules).
+```ShellSession
+a@link> cat fdb-status.py
+#! /usr/bin/env nix-shell
+#! nix-shell -i python -p python pythonPackages.foundationdb52
+
+import fdb
+import json
+
+def main():
+    fdb.api_version(520)
+    db = fdb.open()
+
+    @fdb.transactional
+    def get_status(tr):
+        return str(tr['\xff\xff/status/json'])
+
+    obj = json.loads(get_status(db))
+    print('FoundationDB available: %s' % obj['client']['database_status']['available'])
+
+if __name__ == "__main__":
+    main()
+a@link> chmod +x fdb-status.py
+a@link> ./fdb-status.py
+FoundationDB available: True
+a@link>
+```
+
+FoundationDB is run under the {command}`foundationdb` user and group
+by default, but this may be changed in the NixOS configuration. The systemd
+unit {command}`foundationdb.service` controls the
+{command}`fdbmonitor` process.
+
+By default, the NixOS module for FoundationDB creates a single SSD-storage
+based database for development and basic usage. This storage engine is
+designed for SSDs and will perform poorly on HDDs; however it can handle far
+more data than the alternative "memory" engine and is a better default
+choice for most deployments. (Note that you can change the storage backend
+on-the-fly for a given FoundationDB cluster using
+{command}`fdbcli`.)
+
+Furthermore, only 1 server process and 1 backup agent are started in the
+default configuration. See below for more on scaling to increase this.
+
+FoundationDB stores all data for all server processes under
+{file}`/var/lib/foundationdb`. You can override this using
+{option}`services.foundationdb.dataDir`, e.g.
+```
+services.foundationdb.dataDir = "/data/fdb";
+```
+
+Similarly, logs are stored under {file}`/var/log/foundationdb`
+by default, and there is a corresponding
+{option}`services.foundationdb.logDir` as well.
+
+## Scaling processes and backup agents {#module-services-foundationdb-scaling}
+
+Scaling the number of server processes is quite easy; simply specify
+{option}`services.foundationdb.serverProcesses` to be the number of
+FoundationDB worker processes that should be started on the machine.
+
+FoundationDB worker processes typically require 4GB of RAM per-process at
+minimum for good performance, so this option is set to 1 by default since
+the maximum amount of RAM is unknown. You're advised to abide by this
+restriction, so pick a number of processes so that each has 4GB or more.
+
+A similar option exists in order to scale backup agent processes,
+{option}`services.foundationdb.backupProcesses`. Backup agents are
+not as performance/RAM sensitive, so feel free to experiment with the number
+of available backup processes.
+
+## Clustering {#module-services-foundationdb-clustering}
+
+FoundationDB on NixOS works similarly to other Linux systems, so this
+section will be brief. Please refer to the full FoundationDB documentation
+for more on clustering.
+
+FoundationDB organizes clusters using a set of
+*coordinators*, which are just specially-designated
+worker processes. By default, every installation of FoundationDB on NixOS
+will start as its own individual cluster, with a single coordinator: the
+first worker process on {command}`localhost`.
+
+Coordinators are specified globally using the
+{command}`/etc/foundationdb/fdb.cluster` file, which all servers and
+client applications will use to find and join coordinators. Note that this
+file *can not* be managed by NixOS so easily:
+FoundationDB is designed so that it will rewrite the file at runtime for all
+clients and nodes when cluster coordinators change, with clients
+transparently handling this without intervention. It is fundamentally a
+mutable file, and you should not try to manage it in any way in NixOS.
+
+When dealing with a cluster, there are two main things you want to do:
+
+  - Add a node to the cluster for storage/compute.
+  - Promote an ordinary worker to a coordinator.
+
+A node must already be a member of the cluster in order to properly be
+promoted to a coordinator, so you must always add it first if you wish to
+promote it.
+
+To add a machine to a FoundationDB cluster:
+
+  - Choose one of the servers to start as the initial coordinator.
+  - Copy the {command}`/etc/foundationdb/fdb.cluster` file from this
+    server to all the other servers. Restart FoundationDB on all of these
+    other servers, so they join the cluster.
+  - All of these servers are now connected and working together in the
+    cluster, under the chosen coordinator.
+
+At this point, you can add as many nodes as you want by just repeating the
+above steps. By default there will still be a single coordinator: you can
+use {command}`fdbcli` to change this and add new coordinators.
+
+As a convenience, FoundationDB can automatically assign coordinators based
+on the redundancy mode you wish to achieve for the cluster. Once all the
+nodes have been joined, simply set the replication policy, and then issue
+the {command}`coordinators auto` command
+
+For example, assuming we have 3 nodes available, we can enable double
+redundancy mode, then auto-select coordinators. For double redundancy, 3
+coordinators is ideal: therefore FoundationDB will make
+*every* node a coordinator automatically:
+
+```ShellSession
+fdbcli> configure double ssd
+fdbcli> coordinators auto
+```
+
+This will transparently update all the servers within seconds, and
+appropriately rewrite the {command}`fdb.cluster` file, as well as
+informing all client processes to do the same.
+
+## Client connectivity {#module-services-foundationdb-connectivity}
+
+By default, all clients must use the current {command}`fdb.cluster`
+file to access a given FoundationDB cluster. This file is located by default
+in {command}`/etc/foundationdb/fdb.cluster` on all machines with the
+FoundationDB service enabled, so you may copy the active one from your
+cluster to a new node in order to connect, if it is not part of the cluster.
+
+## Client authorization and TLS {#module-services-foundationdb-authorization}
+
+By default, any user who can connect to a FoundationDB process with the
+correct cluster configuration can access anything. FoundationDB uses a
+pluggable design to transport security, and out of the box it supports a
+LibreSSL-based plugin for TLS support. This plugin not only does in-flight
+encryption, but also performs client authorization based on the given
+endpoint's certificate chain. For example, a FoundationDB server may be
+configured to only accept client connections over TLS, where the client TLS
+certificate is from organization *Acme Co* in the
+*Research and Development* unit.
+
+Configuring TLS with FoundationDB is done using the
+{option}`services.foundationdb.tls` options in order to control the
+peer verification string, as well as the certificate and its private key.
+
+Note that the certificate and its private key must be accessible to the
+FoundationDB user account that the server runs under. These files are also
+NOT managed by NixOS, as putting them into the store may reveal private
+information.
+
+After you have a key and certificate file in place, it is not enough to
+simply set the NixOS module options -- you must also configure the
+{command}`fdb.cluster` file to specify that a given set of
+coordinators use TLS. This is as simple as adding the suffix
+{command}`:tls` to your cluster coordinator configuration, after the
+port number. For example, assuming you have a coordinator on localhost with
+the default configuration, simply specifying:
+
+```
+XXXXXX:XXXXXX@127.0.0.1:4500:tls
+```
+
+will configure all clients and server processes to use TLS from now on.
+
+## Backups and Disaster Recovery {#module-services-foundationdb-disaster-recovery}
+
+The usual rules for doing FoundationDB backups apply on NixOS as written in
+the FoundationDB manual. However, one important difference is the security
+profile for NixOS: by default, the {command}`foundationdb` systemd
+unit uses *Linux namespaces* to restrict write access to
+the system, except for the log directory, data directory, and the
+{command}`/etc/foundationdb/` directory. This is enforced by default
+and cannot be disabled.
+
+However, a side effect of this is that the {command}`fdbbackup`
+command doesn't work properly for local filesystem backups: FoundationDB
+uses a server process alongside the database processes to perform backups
+and copy the backups to the filesystem. As a result, this process is put
+under the restricted namespaces above: the backup process can only write to
+a limited number of paths.
+
+In order to allow flexible backup locations on local disks, the FoundationDB
+NixOS module supports a
+{option}`services.foundationdb.extraReadWritePaths` option. This
+option takes a list of paths, and adds them to the systemd unit, allowing
+the processes inside the service to write (and read) the specified
+directories.
+
+For example, to create backups in {command}`/opt/fdb-backups`, first
+set up the paths in the module options:
+
+```
+services.foundationdb.extraReadWritePaths = [ "/opt/fdb-backups" ];
+```
+
+Restart the FoundationDB service, and it will now be able to write to this
+directory (even if it does not yet exist.) Note: this path
+*must* exist before restarting the unit. Otherwise,
+systemd will not include it in the private FoundationDB namespace (and it
+will not add it dynamically at runtime).
+
+You can now perform a backup:
+
+```ShellSession
+$ sudo -u foundationdb fdbbackup start  -t default -d file:///opt/fdb-backups
+$ sudo -u foundationdb fdbbackup status -t default
+```
+
+## Known limitations {#module-services-foundationdb-limitations}
+
+The FoundationDB setup for NixOS should currently be considered beta.
+FoundationDB is not new software, but the NixOS compilation and integration
+has only undergone fairly basic testing of all the available functionality.
+
+  - There is no way to specify individual parameters for individual
+    {command}`fdbserver` processes. Currently, all server processes
+    inherit all the global {command}`fdbmonitor` settings.
+  - Ruby bindings are not currently installed.
+  - Go bindings are not currently installed.
+
+## Options {#module-services-foundationdb-options}
+
+NixOS's FoundationDB module allows you to configure all of the most relevant
+configuration options for {command}`fdbmonitor`, matching it quite
+closely. A complete list of options for the FoundationDB module may be found
+[here](#opt-services.foundationdb.enable). You should
+also read the FoundationDB documentation as well.
+
+## Full documentation {#module-services-foundationdb-full-docs}
+
+FoundationDB is a complex piece of software, and requires careful
+administration to properly use. Full documentation for administration can be
+found here: <https://apple.github.io/foundationdb/>.
diff --git a/nixos/modules/services/databases/foundationdb.nix b/nixos/modules/services/databases/foundationdb.nix
index 16d539b661e..48e9898a68c 100644
--- a/nixos/modules/services/databases/foundationdb.nix
+++ b/nixos/modules/services/databases/foundationdb.nix
@@ -424,6 +424,6 @@ in
     };
   };
 
-  meta.doc         = ./foundationdb.xml;
+  meta.doc         = ./foundationdb.md;
   meta.maintainers = with lib.maintainers; [ thoughtpolice ];
 }
diff --git a/nixos/modules/services/databases/foundationdb.xml b/nixos/modules/services/databases/foundationdb.xml
deleted file mode 100644
index b0b1ebeab45..00000000000
--- a/nixos/modules/services/databases/foundationdb.xml
+++ /dev/null
@@ -1,443 +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-foundationdb">
- <title>FoundationDB</title>
- <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/services/databases/foundationdb.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://apple.github.io/foundationdb/"/>
- </para>
- <para>
-  <emphasis>Maintainer:</emphasis> Austin Seipp
- </para>
- <para>
-  <emphasis>Available version(s):</emphasis> 5.1.x, 5.2.x, 6.0.x
- </para>
- <para>
-  FoundationDB (or "FDB") is an open source, distributed, transactional
-  key-value store.
- </para>
- <section xml:id="module-services-foundationdb-configuring">
-  <title>Configuring and basic setup</title>
-
-  <para>
-   To enable FoundationDB, add the following to your
-   <filename>configuration.nix</filename>:
-<programlisting>
-services.foundationdb.enable = true;
-services.foundationdb.package = pkgs.foundationdb52; # FoundationDB 5.2.x
-</programlisting>
-  </para>
-
-  <para>
-   The <option>services.foundationdb.package</option> option is required, and
-   must always be specified. Due to the fact FoundationDB network protocols and
-   on-disk storage formats may change between (major) versions, and upgrades
-   must be explicitly handled by the user, you must always manually specify
-   this yourself so that the NixOS module will use the proper version. Note
-   that minor, bugfix releases are always compatible.
-  </para>
-
-  <para>
-   After running <command>nixos-rebuild</command>, you can verify whether
-   FoundationDB is running by executing <command>fdbcli</command> (which is
-   added to <option>environment.systemPackages</option>):
-<screen>
-<prompt>$ </prompt>sudo -u foundationdb fdbcli
-Using cluster file `/etc/foundationdb/fdb.cluster'.
-
-The database is available.
-
-Welcome to the fdbcli. For help, type `help'.
-<prompt>fdb> </prompt>status
-
-Using cluster file `/etc/foundationdb/fdb.cluster'.
-
-Configuration:
-  Redundancy mode        - single
-  Storage engine         - memory
-  Coordinators           - 1
-
-Cluster:
-  FoundationDB processes - 1
-  Machines               - 1
-  Memory availability    - 5.4 GB per process on machine with least available
-  Fault Tolerance        - 0 machines
-  Server time            - 04/20/18 15:21:14
-
-...
-
-<prompt>fdb></prompt>
-</screen>
-  </para>
-
-  <para>
-   You can also write programs using the available client libraries. For
-   example, the following Python program can be run in order to grab the
-   cluster status, as a quick example. (This example uses
-   <command>nix-shell</command> shebang support to automatically supply the
-   necessary Python modules).
-<screen>
-<prompt>a@link> </prompt>cat fdb-status.py
-#! /usr/bin/env nix-shell
-#! nix-shell -i python -p python pythonPackages.foundationdb52
-
-import fdb
-import json
-
-def main():
-    fdb.api_version(520)
-    db = fdb.open()
-
-    @fdb.transactional
-    def get_status(tr):
-        return str(tr['\xff\xff/status/json'])
-
-    obj = json.loads(get_status(db))
-    print('FoundationDB available: %s' % obj['client']['database_status']['available'])
-
-if __name__ == "__main__":
-    main()
-<prompt>a@link> </prompt>chmod +x fdb-status.py
-<prompt>a@link> </prompt>./fdb-status.py
-FoundationDB available: True
-<prompt>a@link></prompt>
-</screen>
-  </para>
-
-  <para>
-   FoundationDB is run under the <command>foundationdb</command> user and group
-   by default, but this may be changed in the NixOS configuration. The systemd
-   unit <command>foundationdb.service</command> controls the
-   <command>fdbmonitor</command> process.
-  </para>
-
-  <para>
-   By default, the NixOS module for FoundationDB creates a single SSD-storage
-   based database for development and basic usage. This storage engine is
-   designed for SSDs and will perform poorly on HDDs; however it can handle far
-   more data than the alternative "memory" engine and is a better default
-   choice for most deployments. (Note that you can change the storage backend
-   on-the-fly for a given FoundationDB cluster using
-   <command>fdbcli</command>.)
-  </para>
-
-  <para>
-   Furthermore, only 1 server process and 1 backup agent are started in the
-   default configuration. See below for more on scaling to increase this.
-  </para>
-
-  <para>
-   FoundationDB stores all data for all server processes under
-   <filename>/var/lib/foundationdb</filename>. You can override this using
-   <option>services.foundationdb.dataDir</option>, e.g.
-<programlisting>
-services.foundationdb.dataDir = "/data/fdb";
-</programlisting>
-  </para>
-
-  <para>
-   Similarly, logs are stored under <filename>/var/log/foundationdb</filename>
-   by default, and there is a corresponding
-   <option>services.foundationdb.logDir</option> as well.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-scaling">
-  <title>Scaling processes and backup agents</title>
-
-  <para>
-   Scaling the number of server processes is quite easy; simply specify
-   <option>services.foundationdb.serverProcesses</option> to be the number of
-   FoundationDB worker processes that should be started on the machine.
-  </para>
-
-  <para>
-   FoundationDB worker processes typically require 4GB of RAM per-process at
-   minimum for good performance, so this option is set to 1 by default since
-   the maximum amount of RAM is unknown. You're advised to abide by this
-   restriction, so pick a number of processes so that each has 4GB or more.
-  </para>
-
-  <para>
-   A similar option exists in order to scale backup agent processes,
-   <option>services.foundationdb.backupProcesses</option>. Backup agents are
-   not as performance/RAM sensitive, so feel free to experiment with the number
-   of available backup processes.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-clustering">
-  <title>Clustering</title>
-
-  <para>
-   FoundationDB on NixOS works similarly to other Linux systems, so this
-   section will be brief. Please refer to the full FoundationDB documentation
-   for more on clustering.
-  </para>
-
-  <para>
-   FoundationDB organizes clusters using a set of
-   <emphasis>coordinators</emphasis>, which are just specially-designated
-   worker processes. By default, every installation of FoundationDB on NixOS
-   will start as its own individual cluster, with a single coordinator: the
-   first worker process on <command>localhost</command>.
-  </para>
-
-  <para>
-   Coordinators are specified globally using the
-   <command>/etc/foundationdb/fdb.cluster</command> file, which all servers and
-   client applications will use to find and join coordinators. Note that this
-   file <emphasis>can not</emphasis> be managed by NixOS so easily:
-   FoundationDB is designed so that it will rewrite the file at runtime for all
-   clients and nodes when cluster coordinators change, with clients
-   transparently handling this without intervention. It is fundamentally a
-   mutable file, and you should not try to manage it in any way in NixOS.
-  </para>
-
-  <para>
-   When dealing with a cluster, there are two main things you want to do:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Add a node to the cluster for storage/compute.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Promote an ordinary worker to a coordinator.
-    </para>
-   </listitem>
-  </itemizedlist>
-
-  <para>
-   A node must already be a member of the cluster in order to properly be
-   promoted to a coordinator, so you must always add it first if you wish to
-   promote it.
-  </para>
-
-  <para>
-   To add a machine to a FoundationDB cluster:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Choose one of the servers to start as the initial coordinator.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Copy the <command>/etc/foundationdb/fdb.cluster</command> file from this
-     server to all the other servers. Restart FoundationDB on all of these
-     other servers, so they join the cluster.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     All of these servers are now connected and working together in the
-     cluster, under the chosen coordinator.
-    </para>
-   </listitem>
-  </itemizedlist>
-
-  <para>
-   At this point, you can add as many nodes as you want by just repeating the
-   above steps. By default there will still be a single coordinator: you can
-   use <command>fdbcli</command> to change this and add new coordinators.
-  </para>
-
-  <para>
-   As a convenience, FoundationDB can automatically assign coordinators based
-   on the redundancy mode you wish to achieve for the cluster. Once all the
-   nodes have been joined, simply set the replication policy, and then issue
-   the <command>coordinators auto</command> command
-  </para>
-
-  <para>
-   For example, assuming we have 3 nodes available, we can enable double
-   redundancy mode, then auto-select coordinators. For double redundancy, 3
-   coordinators is ideal: therefore FoundationDB will make
-   <emphasis>every</emphasis> node a coordinator automatically:
-  </para>
-
-<screen>
-<prompt>fdbcli> </prompt>configure double ssd
-<prompt>fdbcli> </prompt>coordinators auto
-</screen>
-
-  <para>
-   This will transparently update all the servers within seconds, and
-   appropriately rewrite the <command>fdb.cluster</command> file, as well as
-   informing all client processes to do the same.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-connectivity">
-  <title>Client connectivity</title>
-
-  <para>
-   By default, all clients must use the current <command>fdb.cluster</command>
-   file to access a given FoundationDB cluster. This file is located by default
-   in <command>/etc/foundationdb/fdb.cluster</command> on all machines with the
-   FoundationDB service enabled, so you may copy the active one from your
-   cluster to a new node in order to connect, if it is not part of the cluster.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-authorization">
-  <title>Client authorization and TLS</title>
-
-  <para>
-   By default, any user who can connect to a FoundationDB process with the
-   correct cluster configuration can access anything. FoundationDB uses a
-   pluggable design to transport security, and out of the box it supports a
-   LibreSSL-based plugin for TLS support. This plugin not only does in-flight
-   encryption, but also performs client authorization based on the given
-   endpoint's certificate chain. For example, a FoundationDB server may be
-   configured to only accept client connections over TLS, where the client TLS
-   certificate is from organization <emphasis>Acme Co</emphasis> in the
-   <emphasis>Research and Development</emphasis> unit.
-  </para>
-
-  <para>
-   Configuring TLS with FoundationDB is done using the
-   <option>services.foundationdb.tls</option> options in order to control the
-   peer verification string, as well as the certificate and its private key.
-  </para>
-
-  <para>
-   Note that the certificate and its private key must be accessible to the
-   FoundationDB user account that the server runs under. These files are also
-   NOT managed by NixOS, as putting them into the store may reveal private
-   information.
-  </para>
-
-  <para>
-   After you have a key and certificate file in place, it is not enough to
-   simply set the NixOS module options -- you must also configure the
-   <command>fdb.cluster</command> file to specify that a given set of
-   coordinators use TLS. This is as simple as adding the suffix
-   <command>:tls</command> to your cluster coordinator configuration, after the
-   port number. For example, assuming you have a coordinator on localhost with
-   the default configuration, simply specifying:
-  </para>
-
-<programlisting>
-XXXXXX:XXXXXX@127.0.0.1:4500:tls
-</programlisting>
-
-  <para>
-   will configure all clients and server processes to use TLS from now on.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-disaster-recovery">
-  <title>Backups and Disaster Recovery</title>
-
-  <para>
-   The usual rules for doing FoundationDB backups apply on NixOS as written in
-   the FoundationDB manual. However, one important difference is the security
-   profile for NixOS: by default, the <command>foundationdb</command> systemd
-   unit uses <emphasis>Linux namespaces</emphasis> to restrict write access to
-   the system, except for the log directory, data directory, and the
-   <command>/etc/foundationdb/</command> directory. This is enforced by default
-   and cannot be disabled.
-  </para>
-
-  <para>
-   However, a side effect of this is that the <command>fdbbackup</command>
-   command doesn't work properly for local filesystem backups: FoundationDB
-   uses a server process alongside the database processes to perform backups
-   and copy the backups to the filesystem. As a result, this process is put
-   under the restricted namespaces above: the backup process can only write to
-   a limited number of paths.
-  </para>
-
-  <para>
-   In order to allow flexible backup locations on local disks, the FoundationDB
-   NixOS module supports a
-   <option>services.foundationdb.extraReadWritePaths</option> option. This
-   option takes a list of paths, and adds them to the systemd unit, allowing
-   the processes inside the service to write (and read) the specified
-   directories.
-  </para>
-
-  <para>
-   For example, to create backups in <command>/opt/fdb-backups</command>, first
-   set up the paths in the module options:
-  </para>
-
-<programlisting>
-services.foundationdb.extraReadWritePaths = [ "/opt/fdb-backups" ];
-</programlisting>
-
-  <para>
-   Restart the FoundationDB service, and it will now be able to write to this
-   directory (even if it does not yet exist.) Note: this path
-   <emphasis>must</emphasis> exist before restarting the unit. Otherwise,
-   systemd will not include it in the private FoundationDB namespace (and it
-   will not add it dynamically at runtime).
-  </para>
-
-  <para>
-   You can now perform a backup:
-  </para>
-
-<screen>
-<prompt>$ </prompt>sudo -u foundationdb fdbbackup start  -t default -d file:///opt/fdb-backups
-<prompt>$ </prompt>sudo -u foundationdb fdbbackup status -t default
-</screen>
- </section>
- <section xml:id="module-services-foundationdb-limitations">
-  <title>Known limitations</title>
-
-  <para>
-   The FoundationDB setup for NixOS should currently be considered beta.
-   FoundationDB is not new software, but the NixOS compilation and integration
-   has only undergone fairly basic testing of all the available functionality.
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     There is no way to specify individual parameters for individual
-     <command>fdbserver</command> processes. Currently, all server processes
-     inherit all the global <command>fdbmonitor</command> settings.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Ruby bindings are not currently installed.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Go bindings are not currently installed.
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
- <section xml:id="module-services-foundationdb-options">
-  <title>Options</title>
-
-  <para>
-   NixOS's FoundationDB module allows you to configure all of the most relevant
-   configuration options for <command>fdbmonitor</command>, matching it quite
-   closely. A complete list of options for the FoundationDB module may be found
-   <link linkend="opt-services.foundationdb.enable">here</link>. You should
-   also read the FoundationDB documentation as well.
-  </para>
- </section>
- <section xml:id="module-services-foundationdb-full-docs">
-  <title>Full documentation</title>
-
-  <para>
-   FoundationDB is a complex piece of software, and requires careful
-   administration to properly use. Full documentation for administration can be
-   found here: <link xlink:href="https://apple.github.io/foundationdb/"/>.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/databases/lldap.nix b/nixos/modules/services/databases/lldap.nix
new file mode 100644
index 00000000000..960792d0805
--- /dev/null
+++ b/nixos/modules/services/databases/lldap.nix
@@ -0,0 +1,121 @@
+{ config, lib, pkgs, utils, ... }:
+
+let
+  cfg = config.services.lldap;
+  format = pkgs.formats.toml { };
+in
+{
+  options.services.lldap = with lib; {
+    enable = mkEnableOption (mdDoc "lldap");
+
+    package = mkPackageOptionMD pkgs "lldap" { };
+
+    environment = mkOption {
+      type = with types; attrsOf str;
+      default = { };
+      example = {
+        LLDAP_JWT_SECRET_FILE = "/run/lldap/jwt_secret";
+        LLDAP_LDAP_USER_PASS_FILE = "/run/lldap/user_password";
+      };
+      description = lib.mdDoc ''
+        Environment variables passed to the service.
+        Any config option name prefixed with `LLDAP_` takes priority over the one in the configuration file.
+      '';
+    };
+
+    environmentFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = lib.mdDoc ''
+        Environment file as defined in {manpage}`systemd.exec(5)` passed to the service.
+      '';
+    };
+
+    settings = mkOption {
+      description = mdDoc ''
+        Free-form settings written directly to the `lldap_config.toml` file.
+        Refer to <https://github.com/lldap/lldap/blob/main/lldap_config.docker_template.toml> for supported values.
+      '';
+
+      default = { };
+
+      type = types.submodule {
+        freeformType = format.type;
+        options = {
+          ldap_host = mkOption {
+            type = types.str;
+            description = mdDoc "The host address that the LDAP server will be bound to.";
+            default = "::";
+          };
+
+          ldap_port = mkOption {
+            type = types.port;
+            description = mdDoc "The port on which to have the LDAP server.";
+            default = 3890;
+          };
+
+          http_host = mkOption {
+            type = types.str;
+            description = mdDoc "The host address that the HTTP server will be bound to.";
+            default = "::";
+          };
+
+          http_port = mkOption {
+            type = types.port;
+            description = mdDoc "The port on which to have the HTTP server, for user login and administration.";
+            default = 17170;
+          };
+
+          http_url = mkOption {
+            type = types.str;
+            description = mdDoc "The public URL of the server, for password reset links.";
+            default = "http://localhost";
+          };
+
+          ldap_base_dn = mkOption {
+            type = types.str;
+            description = mdDoc "Base DN for LDAP.";
+            example = "dc=example,dc=com";
+          };
+
+          ldap_user_dn = mkOption {
+            type = types.str;
+            description = mdDoc "Admin username";
+            default = "admin";
+          };
+
+          ldap_user_email = mkOption {
+            type = types.str;
+            description = mdDoc "Admin email.";
+            default = "admin@example.com";
+          };
+
+          database_url = mkOption {
+            type = types.str;
+            description = mdDoc "Database URL.";
+            default = "sqlite://./users.db?mode=rwc";
+            example = "postgres://postgres-user:password@postgres-server/my-database";
+          };
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.lldap = {
+      description = "Lightweight LDAP server (lldap)";
+      after = [ "network-online.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${lib.getExe cfg.package} run --config-file ${format.generate "lldap_config.toml" cfg.settings}";
+        StateDirectory = "lldap";
+        WorkingDirectory = "%S/lldap";
+        User = "lldap";
+        Group = "lldap";
+        DynamicUser = true;
+        EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
+      };
+      inherit (cfg) environment;
+    };
+  };
+}
diff --git a/nixos/modules/services/databases/neo4j.nix b/nixos/modules/services/databases/neo4j.nix
index d78ff8390e4..09050242402 100644
--- a/nixos/modules/services/databases/neo4j.nix
+++ b/nixos/modules/services/databases/neo4j.nix
@@ -636,6 +636,6 @@ in {
     };
 
   meta = {
-    maintainers = with lib.maintainers; [ patternspandemic jonringer erictapen ];
+    maintainers = with lib.maintainers; [ patternspandemic jonringer ];
   };
 }
diff --git a/nixos/modules/services/databases/postgresql.md b/nixos/modules/services/databases/postgresql.md
new file mode 100644
index 00000000000..4d66ee38be4
--- /dev/null
+++ b/nixos/modules/services/databases/postgresql.md
@@ -0,0 +1,210 @@
+# PostgreSQL {#module-postgresql}
+
+<!-- FIXME: render nicely -->
+<!-- FIXME: source can be added automatically -->
+
+*Source:* {file}`modules/services/databases/postgresql.nix`
+
+*Upstream documentation:* <http://www.postgresql.org/docs/>
+
+<!-- FIXME: more stuff, like maintainer? -->
+
+PostgreSQL is an advanced, free relational database.
+<!-- MORE -->
+
+## Configuring {#module-services-postgres-configuring}
+
+To enable PostgreSQL, add the following to your {file}`configuration.nix`:
+```
+services.postgresql.enable = true;
+services.postgresql.package = pkgs.postgresql_11;
+```
+Note that you are required to specify the desired version of PostgreSQL (e.g. `pkgs.postgresql_11`). Since upgrading your PostgreSQL version requires a database dump and reload (see below), NixOS cannot provide a default value for [](#opt-services.postgresql.package) such as the most recent release of PostgreSQL.
+
+<!--
+After running {command}`nixos-rebuild`, you can verify
+whether PostgreSQL works by running {command}`psql`:
+
+```ShellSession
+$ psql
+psql (9.2.9)
+Type "help" for help.
+
+alice=>
+```
+-->
+
+By default, PostgreSQL stores its databases in {file}`/var/lib/postgresql/$psqlSchema`. You can override this using [](#opt-services.postgresql.dataDir), e.g.
+```
+services.postgresql.dataDir = "/data/postgresql";
+```
+
+## Upgrading {#module-services-postgres-upgrading}
+
+::: {.note}
+The steps below demonstrate how to upgrade from an older version to `pkgs.postgresql_13`.
+These instructions are also applicable to other versions.
+:::
+
+Major PostgreSQL upgrades require a downtime and a few imperative steps to be called. This is the case because
+each major version has some internal changes in the databases' state during major releases. Because of that,
+NixOS places the state into {file}`/var/lib/postgresql/&lt;version&gt;` where each `version`
+can be obtained like this:
+```
+$ nix-instantiate --eval -A postgresql_13.psqlSchema
+"13"
+```
+For an upgrade, a script like this can be used to simplify the process:
+```
+{ config, pkgs, ... }:
+{
+  environment.systemPackages = [
+    (let
+      # XXX specify the postgresql package you'd like to upgrade to.
+      # Do not forget to list the extensions you need.
+      newPostgres = pkgs.postgresql_13.withPackages (pp: [
+        # pp.plv8
+      ]);
+    in pkgs.writeScriptBin "upgrade-pg-cluster" ''
+      set -eux
+      # XXX it's perhaps advisable to stop all services that depend on postgresql
+      systemctl stop postgresql
+
+      export NEWDATA="/var/lib/postgresql/${newPostgres.psqlSchema}"
+
+      export NEWBIN="${newPostgres}/bin"
+
+      export OLDDATA="${config.services.postgresql.dataDir}"
+      export OLDBIN="${config.services.postgresql.package}/bin"
+
+      install -d -m 0700 -o postgres -g postgres "$NEWDATA"
+      cd "$NEWDATA"
+      sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
+
+      sudo -u postgres $NEWBIN/pg_upgrade \
+        --old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
+        --old-bindir $OLDBIN --new-bindir $NEWBIN \
+        "$@"
+    '')
+  ];
+}
+```
+
+The upgrade process is:
+
+  1. Rebuild nixos configuration with the configuration above added to your {file}`configuration.nix`. Alternatively, add that into separate file and reference it in `imports` list.
+  2. Login as root (`sudo su -`)
+  3. Run `upgrade-pg-cluster`. It will stop old postgresql, initialize a new one and migrate the old one to the new one. You may supply arguments like `--jobs 4` and `--link` to speedup migration process. See <https://www.postgresql.org/docs/current/pgupgrade.html> for details.
+  4. Change postgresql package in NixOS configuration to the one you were upgrading to via [](#opt-services.postgresql.package). Rebuild NixOS. This should start new postgres using upgraded data directory and all services you stopped during the upgrade.
+  5. After the upgrade it's advisable to analyze the new cluster.
+
+       - For PostgreSQL ≥ 14, use the `vacuumdb` command printed by the upgrades script.
+       - For PostgreSQL < 14, run (as `su -l postgres` in the [](#opt-services.postgresql.dataDir), in this example {file}`/var/lib/postgresql/13`):
+
+         ```
+         $ ./analyze_new_cluster.sh
+         ```
+
+     ::: {.warning}
+     The next step removes the old state-directory!
+     :::
+
+     ```
+     $ ./delete_old_cluster.sh
+     ```
+
+## Options {#module-services-postgres-options}
+
+A complete list of options for the PostgreSQL module may be found [here](#opt-services.postgresql.enable).
+
+## Plugins {#module-services-postgres-plugins}
+
+Plugins collection for each PostgreSQL version can be accessed with `.pkgs`. For example, for `pkgs.postgresql_11` package, its plugin collection is accessed by `pkgs.postgresql_11.pkgs`:
+```ShellSession
+$ nix repl '<nixpkgs>'
+
+Loading '<nixpkgs>'...
+Added 10574 variables.
+
+nix-repl> postgresql_11.pkgs.<TAB><TAB>
+postgresql_11.pkgs.cstore_fdw        postgresql_11.pkgs.pg_repack
+postgresql_11.pkgs.pg_auto_failover  postgresql_11.pkgs.pg_safeupdate
+postgresql_11.pkgs.pg_bigm           postgresql_11.pkgs.pg_similarity
+postgresql_11.pkgs.pg_cron           postgresql_11.pkgs.pg_topn
+postgresql_11.pkgs.pg_hll            postgresql_11.pkgs.pgjwt
+postgresql_11.pkgs.pg_partman        postgresql_11.pkgs.pgroonga
+...
+```
+
+To add plugins via NixOS configuration, set `services.postgresql.extraPlugins`:
+```
+services.postgresql.package = pkgs.postgresql_11;
+services.postgresql.extraPlugins = with pkgs.postgresql_11.pkgs; [
+  pg_repack
+  postgis
+];
+```
+
+You can build custom PostgreSQL-with-plugins (to be used outside of NixOS) using function `.withPackages`. For example, creating a custom PostgreSQL package in an overlay can look like:
+```
+self: super: {
+  postgresql_custom = self.postgresql_11.withPackages (ps: [
+    ps.pg_repack
+    ps.postgis
+  ]);
+}
+```
+
+Here's a recipe on how to override a particular plugin through an overlay:
+```
+self: super: {
+  postgresql_11 = super.postgresql_11.override { this = self.postgresql_11; } // {
+    pkgs = super.postgresql_11.pkgs // {
+      pg_repack = super.postgresql_11.pkgs.pg_repack.overrideAttrs (_: {
+        name = "pg_repack-v20181024";
+        src = self.fetchzip {
+          url = "https://github.com/reorg/pg_repack/archive/923fa2f3c709a506e111cc963034bf2fd127aa00.tar.gz";
+          sha256 = "17k6hq9xaax87yz79j773qyigm4fwk8z4zh5cyp6z0sxnwfqxxw5";
+        };
+      });
+    };
+  };
+}
+```
+
+## JIT (Just-In-Time compilation) {#module-services-postgres-jit}
+
+[JIT](https://www.postgresql.org/docs/current/jit-reason.html)-support in the PostgreSQL package
+is disabled by default because of the ~300MiB closure-size increase from the LLVM dependency. It
+can be optionally enabled in PostgreSQL with the following config option:
+
+```nix
+{
+  services.postgresql.enableJIT = true;
+}
+```
+
+This makes sure that the [`jit`](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-JIT)-setting
+is set to `on` and a PostgreSQL package with JIT enabled is used. Further tweaking of the JIT compiler, e.g. setting a different
+query cost threshold via [`jit_above_cost`](https://www.postgresql.org/docs/current/runtime-config-query.html#GUC-JIT-ABOVE-COST)
+can be done manually via [`services.postgresql.settings`](#opt-services.postgresql.settings).
+
+The attribute-names of JIT-enabled PostgreSQL packages are suffixed with `_jit`, i.e. for each `pkgs.postgresql`
+(and `pkgs.postgresql_<major>`) in `nixpkgs` there's also a `pkgs.postgresql_jit` (and `pkgs.postgresql_<major>_jit`).
+Alternatively, a JIT-enabled variant can be derived from a given `postgresql` package via `postgresql.withJIT`.
+This is also useful if it's not clear which attribute from `nixpkgs` was originally used (e.g. when working with
+[`config.services.postgresql.package`](#opt-services.postgresql.package) or if the package was modified via an
+overlay) since all modifications are propagated to `withJIT`. I.e.
+
+```nix
+with import <nixpkgs> {
+  overlays = [
+    (self: super: {
+      postgresql = super.postgresql.overrideAttrs (_: { pname = "foobar"; });
+    })
+  ];
+};
+postgresql.withJIT.pname
+```
+
+evaluates to `"foobar"`.
diff --git a/nixos/modules/services/databases/postgresql.nix b/nixos/modules/services/databases/postgresql.nix
index 6665e7a088f..3d55995aba0 100644
--- a/nixos/modules/services/databases/postgresql.nix
+++ b/nixos/modules/services/databases/postgresql.nix
@@ -7,9 +7,18 @@ let
   cfg = config.services.postgresql;
 
   postgresql =
+    let
+      # ensure that
+      #   services.postgresql = {
+      #     enableJIT = true;
+      #     package = pkgs.postgresql_<major>;
+      #   };
+      # works.
+      base = if cfg.enableJIT && !cfg.package.jitSupport then cfg.package.withJIT else cfg.package;
+    in
     if cfg.extraPlugins == []
-      then cfg.package
-      else cfg.package.withPackages (_: cfg.extraPlugins);
+      then base
+      else base.withPackages (_: cfg.extraPlugins);
 
   toStr = value:
     if true == value then "yes"
@@ -42,6 +51,8 @@ in
 
       enable = mkEnableOption (lib.mdDoc "PostgreSQL Server");
 
+      enableJIT = mkEnableOption (lib.mdDoc "JIT support");
+
       package = mkOption {
         type = types.package;
         example = literalExpression "pkgs.postgresql_11";
@@ -435,19 +446,21 @@ in
         log_line_prefix = cfg.logLinePrefix;
         listen_addresses = if cfg.enableTCPIP then "*" else "localhost";
         port = cfg.port;
+        jit = mkDefault (if cfg.enableJIT then "on" else "off");
       };
 
     services.postgresql.package = let
         mkThrow = ver: throw "postgresql_${ver} was removed, please upgrade your postgresql version.";
+        base = 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";
     in
       # Note: when changing the default, make it conditional on
       # ‘system.stateVersion’ to maintain compatibility with existing
       # systems!
-      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");
+      mkDefault (if cfg.enableJIT then base.withJIT else base);
 
     services.postgresql.dataDir = mkDefault "/var/lib/postgresql/${cfg.package.psqlSchema}";
 
@@ -585,6 +598,6 @@ in
 
   };
 
-  meta.doc = ./postgresql.xml;
+  meta.doc = ./postgresql.md;
   meta.maintainers = with lib.maintainers; [ thoughtpolice danbst ];
 }
diff --git a/nixos/modules/services/databases/postgresql.xml b/nixos/modules/services/databases/postgresql.xml
deleted file mode 100644
index e48c578e6ce..00000000000
--- a/nixos/modules/services/databases/postgresql.xml
+++ /dev/null
@@ -1,231 +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-postgresql">
- <title>PostgreSQL</title>
-<!-- FIXME: render nicely -->
-<!-- FIXME: source can be added automatically -->
- <para>
-  <emphasis>Source:</emphasis> <filename>modules/services/databases/postgresql.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis> <link xlink:href="http://www.postgresql.org/docs/"/>
- </para>
-<!-- FIXME: more stuff, like maintainer? -->
- <para>
-  PostgreSQL is an advanced, free relational database.
-<!-- MORE -->
- </para>
- <section xml:id="module-services-postgres-configuring">
-  <title>Configuring</title>
-
-  <para>
-   To enable PostgreSQL, add the following to your <filename>configuration.nix</filename>:
-<programlisting>
-<xref linkend="opt-services.postgresql.enable"/> = true;
-<xref linkend="opt-services.postgresql.package"/> = pkgs.postgresql_11;
-</programlisting>
-   Note that you are required to specify the desired version of PostgreSQL (e.g. <literal>pkgs.postgresql_11</literal>). Since upgrading your PostgreSQL version requires a database dump and reload (see below), NixOS cannot provide a default value for <xref linkend="opt-services.postgresql.package"/> such as the most recent release of PostgreSQL.
-  </para>
-
-<!--
-<para>After running <command>nixos-rebuild</command>, you can verify
-whether PostgreSQL works by running <command>psql</command>:
-
-<screen>
-<prompt>$ </prompt>psql
-psql (9.2.9)
-Type "help" for help.
-
-<prompt>alice=></prompt>
-</screen>
--->
-
-  <para>
-   By default, PostgreSQL stores its databases in <filename>/var/lib/postgresql/$psqlSchema</filename>. You can override this using <xref linkend="opt-services.postgresql.dataDir"/>, e.g.
-<programlisting>
-<xref linkend="opt-services.postgresql.dataDir"/> = "/data/postgresql";
-</programlisting>
-  </para>
- </section>
- <section xml:id="module-services-postgres-upgrading">
-  <title>Upgrading</title>
-
-  <note>
-   <para>
-    The steps below demonstrate how to upgrade from an older version to <package>pkgs.postgresql_13</package>.
-    These instructions are also applicable to other versions.
-   </para>
-  </note>
-  <para>
-   Major PostgreSQL upgrades require a downtime and a few imperative steps to be called. This is the case because
-   each major version has some internal changes in the databases' state during major releases. Because of that,
-   NixOS places the state into <filename>/var/lib/postgresql/&lt;version&gt;</filename> where each <literal>version</literal>
-   can be obtained like this:
-<programlisting>
-<prompt>$ </prompt>nix-instantiate --eval -A postgresql_13.psqlSchema
-"13"
-</programlisting>
-   For an upgrade, a script like this can be used to simplify the process:
-<programlisting>
-{ config, pkgs, ... }:
-{
-  <xref linkend="opt-environment.systemPackages" /> = [
-    (let
-      # XXX specify the postgresql package you'd like to upgrade to.
-      # Do not forget to list the extensions you need.
-      newPostgres = pkgs.postgresql_13.withPackages (pp: [
-        # pp.plv8
-      ]);
-    in pkgs.writeScriptBin "upgrade-pg-cluster" ''
-      set -eux
-      # XXX it's perhaps advisable to stop all services that depend on postgresql
-      systemctl stop postgresql
-
-      export NEWDATA="/var/lib/postgresql/${newPostgres.psqlSchema}"
-
-      export NEWBIN="${newPostgres}/bin"
-
-      export OLDDATA="${config.<xref linkend="opt-services.postgresql.dataDir"/>}"
-      export OLDBIN="${config.<xref linkend="opt-services.postgresql.package"/>}/bin"
-
-      install -d -m 0700 -o postgres -g postgres "$NEWDATA"
-      cd "$NEWDATA"
-      sudo -u postgres $NEWBIN/initdb -D "$NEWDATA"
-
-      sudo -u postgres $NEWBIN/pg_upgrade \
-        --old-datadir "$OLDDATA" --new-datadir "$NEWDATA" \
-        --old-bindir $OLDBIN --new-bindir $NEWBIN \
-        "$@"
-    '')
-  ];
-}
-</programlisting>
-  </para>
-
-  <para>
-   The upgrade process is:
-  </para>
-
-  <orderedlist>
-   <listitem>
-    <para>
-     Rebuild nixos configuration with the configuration above added to your <filename>configuration.nix</filename>. Alternatively, add that into separate file and reference it in <literal>imports</literal> list.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Login as root (<literal>sudo su -</literal>)
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Run <literal>upgrade-pg-cluster</literal>. It will stop old postgresql, initialize a new one and migrate the old one to the new one. You may supply arguments like <literal>--jobs 4</literal> and <literal>--link</literal> to speedup migration process. See <link xlink:href="https://www.postgresql.org/docs/current/pgupgrade.html" /> for details.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     Change postgresql package in NixOS configuration to the one you were upgrading to via <xref linkend="opt-services.postgresql.package" />. Rebuild NixOS. This should start new postgres using upgraded data directory and all services you stopped during the upgrade.
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     After the upgrade it's advisable to analyze the new cluster.
-    </para>
-    <itemizedlist>
-     <listitem>
-      <para>
-       For PostgreSQL ≥ 14, use the <literal>vacuumdb</literal> command printed by the upgrades script.
-      </para>
-     </listitem>
-     <listitem>
-       <para>
-        For PostgreSQL &lt; 14, run (as <literal>su -l postgres</literal> in the <xref linkend="opt-services.postgresql.dataDir" />, in this example <filename>/var/lib/postgresql/13</filename>):
-<programlisting>
-<prompt>$ </prompt>./analyze_new_cluster.sh
-</programlisting>
-       </para>
-     </listitem>
-    </itemizedlist>
-    <para>
-      <warning><para>The next step removes the old state-directory!</para></warning>
-<programlisting>
-<prompt>$ </prompt>./delete_old_cluster.sh
-</programlisting>
-    </para>
-   </listitem>
-  </orderedlist>
- </section>
- <section xml:id="module-services-postgres-options">
-  <title>Options</title>
-
-  <para>
-   A complete list of options for the PostgreSQL module may be found <link linkend="opt-services.postgresql.enable">here</link>.
-  </para>
- </section>
- <section xml:id="module-services-postgres-plugins">
-  <title>Plugins</title>
-
-  <para>
-   Plugins collection for each PostgreSQL version can be accessed with <literal>.pkgs</literal>. For example, for <literal>pkgs.postgresql_11</literal> package, its plugin collection is accessed by <literal>pkgs.postgresql_11.pkgs</literal>:
-<screen>
-<prompt>$ </prompt>nix repl '&lt;nixpkgs&gt;'
-
-Loading '&lt;nixpkgs&gt;'...
-Added 10574 variables.
-
-<prompt>nix-repl&gt; </prompt>postgresql_11.pkgs.&lt;TAB&gt;&lt;TAB&gt;
-postgresql_11.pkgs.cstore_fdw        postgresql_11.pkgs.pg_repack
-postgresql_11.pkgs.pg_auto_failover  postgresql_11.pkgs.pg_safeupdate
-postgresql_11.pkgs.pg_bigm           postgresql_11.pkgs.pg_similarity
-postgresql_11.pkgs.pg_cron           postgresql_11.pkgs.pg_topn
-postgresql_11.pkgs.pg_hll            postgresql_11.pkgs.pgjwt
-postgresql_11.pkgs.pg_partman        postgresql_11.pkgs.pgroonga
-...
-</screen>
-  </para>
-
-  <para>
-   To add plugins via NixOS configuration, set <literal>services.postgresql.extraPlugins</literal>:
-<programlisting>
-<xref linkend="opt-services.postgresql.package"/> = pkgs.postgresql_11;
-<xref linkend="opt-services.postgresql.extraPlugins"/> = with pkgs.postgresql_11.pkgs; [
-  pg_repack
-  postgis
-];
-</programlisting>
-  </para>
-
-  <para>
-   You can build custom PostgreSQL-with-plugins (to be used outside of NixOS) using function <literal>.withPackages</literal>. For example, creating a custom PostgreSQL package in an overlay can look like:
-<programlisting>
-self: super: {
-  postgresql_custom = self.postgresql_11.withPackages (ps: [
-    ps.pg_repack
-    ps.postgis
-  ]);
-}
-</programlisting>
-  </para>
-
-  <para>
-   Here's a recipe on how to override a particular plugin through an overlay:
-<programlisting>
-self: super: {
-  postgresql_11 = super.postgresql_11.override { this = self.postgresql_11; } // {
-    pkgs = super.postgresql_11.pkgs // {
-      pg_repack = super.postgresql_11.pkgs.pg_repack.overrideAttrs (_: {
-        name = "pg_repack-v20181024";
-        src = self.fetchzip {
-          url = "https://github.com/reorg/pg_repack/archive/923fa2f3c709a506e111cc963034bf2fd127aa00.tar.gz";
-          sha256 = "17k6hq9xaax87yz79j773qyigm4fwk8z4zh5cyp6z0sxnwfqxxw5";
-        };
-      });
-    };
-  };
-}
-</programlisting>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/desktops/deepin/app-services.nix b/nixos/modules/services/desktops/deepin/app-services.nix
new file mode 100644
index 00000000000..6f9932e4873
--- /dev/null
+++ b/nixos/modules/services/desktops/deepin/app-services.nix
@@ -0,0 +1,36 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.deepin.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    services.deepin.app-services = {
+
+      enable = mkEnableOption (lib.mdDoc "Service collection of DDE applications, including dconfig-center");
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.deepin.app-services.enable {
+
+    environment.systemPackages = [ pkgs.deepin.dde-app-services ];
+
+    services.dbus.packages = [ pkgs.deepin.dde-app-services ];
+
+    environment.pathsToLink = [ "/share/dsg" ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/deepin/dde-api.nix b/nixos/modules/services/desktops/deepin/dde-api.nix
new file mode 100644
index 00000000000..57b2290dfbc
--- /dev/null
+++ b/nixos/modules/services/desktops/deepin/dde-api.nix
@@ -0,0 +1,50 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.deepin.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    services.deepin.dde-api = {
+
+      enable = mkEnableOption (lib.mdDoc ''
+        Provides some dbus interfaces that is used for screen zone detecting,
+        thumbnail generating, and sound playing in Deepin Desktop Enviroment.
+      '');
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.deepin.dde-api.enable {
+
+     environment.systemPackages = [ pkgs.deepin.dde-api ];
+
+     services.dbus.packages = [ pkgs.deepin.dde-api ];
+
+     systemd.packages = [ pkgs.deepin.dde-api ];
+
+     environment.pathsToLink = [ "/lib/deepin-api" ];
+
+     users.groups.deepin-sound-player = { };
+     users.users.deepin-sound-player = {
+       description = "Deepin sound player";
+       home = "/var/lib/deepin-sound-player";
+       createHome = true;
+       group = "deepin-sound-player";
+       isSystemUser = true;
+     };
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/deepin/dde-daemon.nix b/nixos/modules/services/desktops/deepin/dde-daemon.nix
new file mode 100644
index 00000000000..9377f523ebf
--- /dev/null
+++ b/nixos/modules/services/desktops/deepin/dde-daemon.nix
@@ -0,0 +1,40 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+{
+
+  meta = {
+    maintainers = teams.deepin.members;
+  };
+
+  ###### interface
+
+  options = {
+
+    services.deepin.dde-daemon = {
+
+      enable = mkEnableOption (lib.mdDoc "Daemon for handling the deepin session settings");
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf config.services.deepin.dde-daemon.enable {
+
+    environment.systemPackages = [ pkgs.deepin.dde-daemon ];
+
+    services.dbus.packages = [ pkgs.deepin.dde-daemon ];
+
+    services.udev.packages = [ pkgs.deepin.dde-daemon ];
+
+    systemd.packages = [ pkgs.deepin.dde-daemon ];
+
+    environment.pathsToLink = [ "/lib/deepin-daemon" ];
+
+  };
+
+}
diff --git a/nixos/modules/services/desktops/flatpak.md b/nixos/modules/services/desktops/flatpak.md
new file mode 100644
index 00000000000..65b1554d79b
--- /dev/null
+++ b/nixos/modules/services/desktops/flatpak.md
@@ -0,0 +1,39 @@
+# Flatpak {#module-services-flatpak}
+
+*Source:* {file}`modules/services/desktop/flatpak.nix`
+
+*Upstream documentation:* <https://github.com/flatpak/flatpak/wiki>
+
+Flatpak is a system for building, distributing, and running sandboxed desktop
+applications on Linux.
+
+To enable Flatpak, add the following to your {file}`configuration.nix`:
+```
+  services.flatpak.enable = true;
+```
+
+For the sandboxed apps to work correctly, desktop integration portals need to
+be installed. If you run GNOME, this will be handled automatically for you;
+in other cases, you will need to add something like the following to your
+{file}`configuration.nix`:
+```
+  xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
+```
+
+Then, you will need to add a repository, for example,
+[Flathub](https://github.com/flatpak/flatpak/wiki),
+either using the following commands:
+```ShellSession
+$ flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
+$ flatpak update
+```
+or by opening the
+[repository file](https://flathub.org/repo/flathub.flatpakrepo) in GNOME Software.
+
+Finally, you can search and install programs:
+```ShellSession
+$ flatpak search bustle
+$ flatpak install flathub org.freedesktop.Bustle
+$ flatpak run org.freedesktop.Bustle
+```
+Again, GNOME Software offers graphical interface for these tasks.
diff --git a/nixos/modules/services/desktops/flatpak.nix b/nixos/modules/services/desktops/flatpak.nix
index 3b14ad75ab3..d99faf381e0 100644
--- a/nixos/modules/services/desktops/flatpak.nix
+++ b/nixos/modules/services/desktops/flatpak.nix
@@ -7,7 +7,7 @@ let
   cfg = config.services.flatpak;
 in {
   meta = {
-    doc = ./flatpak.xml;
+    doc = ./flatpak.md;
     maintainers = pkgs.flatpak.meta.maintainers;
   };
 
diff --git a/nixos/modules/services/desktops/flatpak.xml b/nixos/modules/services/desktops/flatpak.xml
deleted file mode 100644
index 8f080b25022..00000000000
--- a/nixos/modules/services/desktops/flatpak.xml
+++ /dev/null
@@ -1,56 +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-flatpak">
- <title>Flatpak</title>
- <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/services/desktop/flatpak.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://github.com/flatpak/flatpak/wiki"/>
- </para>
- <para>
-  Flatpak is a system for building, distributing, and running sandboxed desktop
-  applications on Linux.
- </para>
- <para>
-  To enable Flatpak, add the following to your
-  <filename>configuration.nix</filename>:
-<programlisting>
-  <xref linkend="opt-services.flatpak.enable"/> = true;
-</programlisting>
- </para>
- <para>
-  For the sandboxed apps to work correctly, desktop integration portals need to
-  be installed. If you run GNOME, this will be handled automatically for you;
-  in other cases, you will need to add something like the following to your
-  <filename>configuration.nix</filename>:
-<programlisting>
-  <xref linkend="opt-xdg.portal.extraPortals"/> = [ pkgs.xdg-desktop-portal-gtk ];
-</programlisting>
- </para>
- <para>
-  Then, you will need to add a repository, for example,
-  <link xlink:href="https://github.com/flatpak/flatpak/wiki">Flathub</link>,
-  either using the following commands:
-<screen>
-<prompt>$ </prompt>flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
-<prompt>$ </prompt>flatpak update
-</screen>
-  or by opening the
-  <link xlink:href="https://flathub.org/repo/flathub.flatpakrepo">repository
-  file</link> in GNOME Software.
- </para>
- <para>
-  Finally, you can search and install programs:
-<screen>
-<prompt>$ </prompt>flatpak search bustle
-<prompt>$ </prompt>flatpak install flathub org.freedesktop.Bustle
-<prompt>$ </prompt>flatpak run org.freedesktop.Bustle
-</screen>
-  Again, GNOME Software offers graphical interface for these tasks.
- </para>
-</chapter>
diff --git a/nixos/modules/services/desktops/gnome/evolution-data-server.nix b/nixos/modules/services/desktops/gnome/evolution-data-server.nix
index 0006ba1a7ba..a8db7dce8fd 100644
--- a/nixos/modules/services/desktops/gnome/evolution-data-server.nix
+++ b/nixos/modules/services/desktops/gnome/evolution-data-server.nix
@@ -27,7 +27,7 @@ with lib;
   options = {
 
     services.gnome.evolution-data-server = {
-      enable = mkEnableOption (lib.mdDoc "Evolution Data Server, a collection of services for storing addressbooks and calendars.");
+      enable = mkEnableOption (lib.mdDoc "Evolution Data Server, a collection of services for storing addressbooks and calendars");
       plugins = mkOption {
         type = types.listOf types.package;
         default = [ ];
@@ -35,7 +35,7 @@ with lib;
       };
     };
     programs.evolution = {
-      enable = mkEnableOption (lib.mdDoc "Evolution, a Personal information management application that provides integrated mail, calendaring and address book functionality.");
+      enable = mkEnableOption (lib.mdDoc "Evolution, a Personal information management application that provides integrated mail, calendaring and address book functionality");
       plugins = mkOption {
         type = types.listOf types.package;
         default = [ ];
diff --git a/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json b/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json
deleted file mode 100644
index 9aa51b61431..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/client-rt.conf.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
-  "context.properties": {
-    "log.level": 0
-  },
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rt",
-      "args": {},
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-client-device"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    },
-    {
-      "name": "libpipewire-module-session-manager"
-    }
-  ],
-  "filter.properties": {},
-  "stream.properties": {}
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/client.conf.json b/nixos/modules/services/desktops/pipewire/daemon/client.conf.json
deleted file mode 100644
index 71294a0e78a..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/client.conf.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
-  "context.properties": {
-    "log.level": 0
-  },
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-client-device"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    },
-    {
-      "name": "libpipewire-module-session-manager"
-    }
-  ],
-  "filter.properties": {},
-  "stream.properties": {}
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json b/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json
deleted file mode 100644
index 689fca88359..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/filter-chain.conf.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
-  "context.properties": {
-    "log.level": 0
-  },
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rt",
-      "args": {},
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/jack.conf.json b/nixos/modules/services/desktops/pipewire/daemon/jack.conf.json
deleted file mode 100644
index 128178bfa02..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/jack.conf.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "context.properties": {
-    "log.level": 0
-  },
-  "context.spa-libs": {
-    "support.*": "support/libspa-support"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rt",
-      "args": {},
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    }
-  ],
-  "jack.properties": {},
-  "jack.rules": [
-    {
-      "matches": [
-        {}
-      ],
-      "actions": {
-        "update-props": {}
-      }
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json b/nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json
deleted file mode 100644
index 0f1ebe5749c..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/minimal.conf.json
+++ /dev/null
@@ -1,120 +0,0 @@
-{
-  "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-avb.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json
deleted file mode 100644
index 4f669895d87..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/pipewire-avb.conf.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
-  "context.properties": {},
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "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-client-node"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-avb",
-      "args": {}
-    }
-  ],
-  "context.exec": [],
-  "stream.properties": {},
-  "avb.properties": {
-    "ifname": "enp3s0",
-    "vm.overrides": {}
-  }
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json
deleted file mode 100644
index 114afbfb0ea..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/pipewire-pulse.conf.json
+++ /dev/null
@@ -1,103 +0,0 @@
-{
-  "context.properties": {},
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "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-client-node"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    },
-    {
-      "name": "libpipewire-module-protocol-pulse",
-      "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"
-        }
-      }
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json b/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json
deleted file mode 100644
index bf3b2d66082..00000000000
--- a/nixos/modules/services/desktops/pipewire/daemon/pipewire.conf.json
+++ /dev/null
@@ -1,97 +0,0 @@
-{
-  "context.properties": {
-    "link.max-buffers": 16,
-    "core.daemon": true,
-    "core.name": "pipewire-0",
-    "default.clock.min-quantum": 16,
-    "vm.overrides": {
-      "default.clock.min-quantum": 1024
-    }
-  },
-  "context.spa-libs": {
-    "audio.convert.*": "audioconvert/libspa-audioconvert",
-    "avb.*": "avb/libspa-avb",
-    "api.alsa.*": "alsa/libspa-alsa",
-    "api.v4l2.*": "v4l2/libspa-v4l2",
-    "api.libcamera.*": "libcamera/libspa-libcamera",
-    "api.bluez5.*": "bluez5/libspa-bluez5",
-    "api.vulkan.*": "vulkan/libspa-vulkan",
-    "api.jack.*": "jack/libspa-jack",
-    "support.*": "support/libspa-support"
-  },
-  "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-device-factory"
-    },
-    {
-      "name": "libpipewire-module-spa-node-factory"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-client-device"
-    },
-    {
-      "name": "libpipewire-module-portal",
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-access",
-      "args": {}
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-link-factory"
-    },
-    {
-      "name": "libpipewire-module-session-manager"
-    }
-  ],
-  "context.objects": [
-    {
-      "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
-      }
-    }
-  ],
-  "context.exec": []
-}
diff --git a/nixos/modules/services/desktops/pipewire/media-session/alsa-monitor.conf.json b/nixos/modules/services/desktops/pipewire/media-session/alsa-monitor.conf.json
deleted file mode 100644
index 53fc9cc9634..00000000000
--- a/nixos/modules/services/desktops/pipewire/media-session/alsa-monitor.conf.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
-  "properties": {},
-  "rules": [
-    {
-      "matches": [
-        {
-          "device.name": "~alsa_card.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "api.alsa.use-acp": true,
-          "api.acp.auto-profile": false,
-          "api.acp.auto-port": false
-        }
-      }
-    },
-    {
-      "matches": [
-        {
-          "node.name": "~alsa_input.*"
-        },
-        {
-          "node.name": "~alsa_output.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "node.pause-on-idle": false
-        }
-      }
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/media-session/bluez-monitor.conf.json b/nixos/modules/services/desktops/pipewire/media-session/bluez-monitor.conf.json
deleted file mode 100644
index 6d1c23e8256..00000000000
--- a/nixos/modules/services/desktops/pipewire/media-session/bluez-monitor.conf.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
-  "properties": {},
-  "rules": [
-    {
-      "matches": [
-        {
-          "device.name": "~bluez_card.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "bluez5.auto-connect": [
-            "hfp_hf",
-            "hsp_hs",
-            "a2dp_sink"
-          ]
-        }
-      }
-    },
-    {
-      "matches": [
-        {
-          "node.name": "~bluez_input.*"
-        },
-        {
-          "node.name": "~bluez_output.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "node.pause-on-idle": false
-        }
-      }
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/media-session/media-session.conf.json b/nixos/modules/services/desktops/pipewire/media-session/media-session.conf.json
deleted file mode 100644
index 4b4e302af38..00000000000
--- a/nixos/modules/services/desktops/pipewire/media-session/media-session.conf.json
+++ /dev/null
@@ -1,68 +0,0 @@
-{
-  "context.properties": {},
-  "context.spa-libs": {
-    "api.bluez5.*": "bluez5/libspa-bluez5",
-    "api.alsa.*": "alsa/libspa-alsa",
-    "api.v4l2.*": "v4l2/libspa-v4l2",
-    "api.libcamera.*": "libcamera/libspa-libcamera"
-  },
-  "context.modules": [
-    {
-      "name": "libpipewire-module-rtkit",
-      "args": {},
-      "flags": [
-        "ifexists",
-        "nofail"
-      ]
-    },
-    {
-      "name": "libpipewire-module-protocol-native"
-    },
-    {
-      "name": "libpipewire-module-client-node"
-    },
-    {
-      "name": "libpipewire-module-client-device"
-    },
-    {
-      "name": "libpipewire-module-adapter"
-    },
-    {
-      "name": "libpipewire-module-metadata"
-    },
-    {
-      "name": "libpipewire-module-session-manager"
-    }
-  ],
-  "session.modules": {
-    "default": [
-      "flatpak",
-      "portal",
-      "v4l2",
-      "suspend-node",
-      "policy-node"
-    ],
-    "with-audio": [
-      "metadata",
-      "default-nodes",
-      "default-profile",
-      "default-routes",
-      "alsa-seq",
-      "alsa-monitor"
-    ],
-    "with-alsa": [
-      "with-audio"
-    ],
-    "with-jack": [
-      "with-audio"
-    ],
-    "with-pulseaudio": [
-      "with-audio",
-      "bluez5",
-      "bluez5-autoswitch",
-      "logind",
-      "restore-stream",
-      "streams-follow-default"
-    ]
-  }
-}
diff --git a/nixos/modules/services/desktops/pipewire/media-session/v4l2-monitor.conf.json b/nixos/modules/services/desktops/pipewire/media-session/v4l2-monitor.conf.json
deleted file mode 100644
index b08cba1b604..00000000000
--- a/nixos/modules/services/desktops/pipewire/media-session/v4l2-monitor.conf.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
-  "properties": {},
-  "rules": [
-    {
-      "matches": [
-        {
-          "device.name": "~v4l2_device.*"
-        }
-      ],
-      "actions": {
-        "update-props": {}
-      }
-    },
-    {
-      "matches": [
-        {
-          "node.name": "~v4l2_input.*"
-        },
-        {
-          "node.name": "~v4l2_output.*"
-        }
-      ],
-      "actions": {
-        "update-props": {
-          "node.pause-on-idle": false
-        }
-      }
-    }
-  ]
-}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix b/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
deleted file mode 100644
index 203139294c6..00000000000
--- a/nixos/modules/services/desktops/pipewire/pipewire-media-session.nix
+++ /dev/null
@@ -1,141 +0,0 @@
-# pipewire example session manager.
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-  json = pkgs.formats.json {};
-  cfg = config.services.pipewire.media-session;
-  enable32BitAlsaPlugins = cfg.alsa.support32Bit
-                           && pkgs.stdenv.isx86_64
-                           && pkgs.pkgsi686Linux.pipewire != null;
-
-  # Use upstream config files passed through spa-json-dump as the base
-  # Patched here as necessary for them to work with this module
-  defaults = {
-    alsa-monitor = lib.importJSON ./media-session/alsa-monitor.conf.json;
-    bluez-monitor = lib.importJSON ./media-session/bluez-monitor.conf.json;
-    media-session = lib.importJSON ./media-session/media-session.conf.json;
-    v4l2-monitor = lib.importJSON ./media-session/v4l2-monitor.conf.json;
-  };
-
-  configs = {
-    alsa-monitor = recursiveUpdate defaults.alsa-monitor cfg.config.alsa-monitor;
-    bluez-monitor = recursiveUpdate defaults.bluez-monitor cfg.config.bluez-monitor;
-    media-session = recursiveUpdate defaults.media-session cfg.config.media-session;
-    v4l2-monitor = recursiveUpdate defaults.v4l2-monitor cfg.config.v4l2-monitor;
-  };
-in {
-
-  meta = {
-    maintainers = teams.freedesktop.members;
-    # uses attributes of the linked package
-    buildDocsInSandbox = false;
-  };
-
-  ###### interface
-  options = {
-    services.pipewire.media-session = {
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Whether to enable the deprecated example Pipewire session manager";
-      };
-
-      package = mkOption {
-        type = types.package;
-        default = pkgs.pipewire-media-session;
-        defaultText = literalExpression "pkgs.pipewire-media-session";
-        description = lib.mdDoc ''
-          The pipewire-media-session derivation to use.
-        '';
-      };
-
-      config = {
-        media-session = mkOption {
-          type = json.type;
-          description = lib.mdDoc ''
-            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 = defaults.media-session;
-        };
-
-        alsa-monitor = mkOption {
-          type = json.type;
-          description = lib.mdDoc ''
-            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 = defaults.alsa-monitor;
-        };
-
-        bluez-monitor = mkOption {
-          type = json.type;
-          description = lib.mdDoc ''
-            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 = defaults.bluez-monitor;
-        };
-
-        v4l2-monitor = mkOption {
-          type = json.type;
-          description = lib.mdDoc ''
-            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 = defaults.v4l2-monitor;
-        };
-      };
-    };
-  };
-
-  ###### implementation
-  config = mkIf cfg.enable {
-    environment.systemPackages = [ cfg.package ];
-    systemd.packages = [ cfg.package ];
-
-    # Enable either system or user units.
-    systemd.services.pipewire-media-session.enable = config.services.pipewire.systemWide;
-    systemd.user.services.pipewire-media-session.enable = !config.services.pipewire.systemWide;
-
-    systemd.services.pipewire-media-session.wantedBy = [ "pipewire.service" ];
-    systemd.user.services.pipewire-media-session.wantedBy = [ "pipewire.service" ];
-
-    environment.etc."pipewire/media-session.d/media-session.conf" = {
-      source = json.generate "media-session.conf" configs.media-session;
-    };
-    environment.etc."pipewire/media-session.d/v4l2-monitor.conf" = {
-      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 = "";
-      };
-    environment.etc."pipewire/media-session.d/alsa-monitor.conf" =
-      mkIf config.services.pipewire.alsa.enable {
-        source = json.generate "alsa-monitor.conf" configs.alsa-monitor;
-      };
-
-    environment.etc."pipewire/media-session.d/with-pulseaudio" =
-      mkIf config.services.pipewire.pulse.enable {
-        text = "";
-      };
-    environment.etc."pipewire/media-session.d/bluez-monitor.conf" =
-      mkIf config.services.pipewire.pulse.enable {
-        source = json.generate "bluez-monitor.conf" configs.bluez-monitor;
-      };
-
-    environment.etc."pipewire/media-session.d/with-jack" =
-      mkIf config.services.pipewire.jack.enable {
-        text = "";
-      };
-  };
-}
diff --git a/nixos/modules/services/desktops/pipewire/pipewire.nix b/nixos/modules/services/desktops/pipewire/pipewire.nix
index a4ef88a45ad..ae695baf42c 100644
--- a/nixos/modules/services/desktops/pipewire/pipewire.nix
+++ b/nixos/modules/services/desktops/pipewire/pipewire.nix
@@ -4,7 +4,6 @@
 with lib;
 
 let
-  json = pkgs.formats.json {};
   cfg = config.services.pipewire;
   enable32BitAlsaPlugins = cfg.alsa.support32Bit
                            && pkgs.stdenv.isx86_64
@@ -18,34 +17,8 @@ let
     mkdir -p "$out/lib"
     ln -s "${cfg.package.jack}/lib" "$out/lib/pipewire"
   '';
-
-  # Use upstream config files passed through spa-json-dump as the base
-  # Patched here as necessary for them to work with this module
-  defaults = {
-    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 (if useSessionManager then defaults.pipewire else defaults.minimal) cfg.config.pipewire;
-    pipewire-pulse = recursiveUpdate defaults.pipewire-pulse cfg.config.pipewire-pulse;
-  };
 in {
-
-  meta = {
-    maintainers = teams.freedesktop.members;
-    # uses attributes of the linked package
-    buildDocsInSandbox = false;
-  };
+  meta.maintainers = teams.freedesktop.members ++ [ lib.maintainers.k900 ];
 
   ###### interface
   options = {
@@ -69,53 +42,6 @@ in {
         '';
       };
 
-      config = {
-        client = mkOption {
-          type = json.type;
-          default = {};
-          description = lib.mdDoc ''
-            Configuration for pipewire clients. For details see
-            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/client.conf.in
-          '';
-        };
-
-        client-rt = mkOption {
-          type = json.type;
-          default = {};
-          description = lib.mdDoc ''
-            Configuration for realtime pipewire clients. For details see
-            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/client-rt.conf.in
-          '';
-        };
-
-        jack = mkOption {
-          type = json.type;
-          default = {};
-          description = lib.mdDoc ''
-            Configuration for the pipewire daemon's jack module. For details see
-            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/jack.conf.in
-          '';
-        };
-
-        pipewire = mkOption {
-          type = json.type;
-          default = {};
-          description = lib.mdDoc ''
-            Configuration for the pipewire daemon. For details see
-            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/pipewire.conf.in
-          '';
-        };
-
-        pipewire-pulse = mkOption {
-          type = json.type;
-          default = {};
-          description = lib.mdDoc ''
-            Configuration for the pipewire-pulse daemon. For details see
-            https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/${cfg.package.version}/src/daemon/pipewire-pulse.conf.in
-          '';
-        };
-      };
-
       audio = {
         enable = lib.mkOption {
           type = lib.types.bool;
@@ -153,10 +79,20 @@ in {
           https://github.com/PipeWire/pipewire/blob/master/NEWS
         '';
       };
-
     };
   };
 
+  imports = [
+    (lib.mkRemovedOptionModule ["services" "pipewire" "config"] ''
+      Overriding default Pipewire configuration through NixOS options never worked correctly and is no longer supported.
+      Please create drop-in files in /etc/pipewire/pipewire.conf.d/ to make the desired setting changes instead.
+    '')
+
+    (lib.mkRemovedOptionModule ["services" "pipewire" "media-session"] ''
+      pipewire-media-session is no longer supported upstream and has been removed.
+      Please switch to `services.pipewire.wireplumber` instead.
+    '')
+  ];
 
   ###### implementation
   config = mkIf cfg.enable {
@@ -222,22 +158,6 @@ in {
       source = "${cfg.package}/share/alsa/alsa.conf.d/99-pipewire-default.conf";
     };
 
-    environment.etc."pipewire/client.conf" = {
-      source = json.generate "client.conf" configs.client;
-    };
-    environment.etc."pipewire/client-rt.conf" = {
-      source = json.generate "client-rt.conf" configs.client-rt;
-    };
-    environment.etc."pipewire/jack.conf" = {
-      source = json.generate "jack.conf" configs.jack;
-    };
-    environment.etc."pipewire/pipewire.conf" = {
-      source = json.generate "pipewire.conf" configs.pipewire;
-    };
-    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.mkIf cfg.jack.enable [ "${cfg.package.jack}/lib" ];
 
@@ -256,12 +176,5 @@ in {
       };
       groups.pipewire.gid = config.ids.gids.pipewire;
     };
-
-    # 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 4b36b99aa7c..95a7ece26c5 100644
--- a/nixos/modules/services/desktops/pipewire/wireplumber.nix
+++ b/nixos/modules/services/desktops/pipewire/wireplumber.nix
@@ -29,10 +29,6 @@ in
   config = lib.mkIf cfg.enable {
     assertions = [
       {
-        assertion = !config.services.pipewire.media-session.enable;
-        message = "WirePlumber and pipewire-media-session can't be enabled at the same time.";
-      }
-      {
         assertion = !config.hardware.bluetooth.hsphfpd.enable;
         message = "Using Wireplumber conflicts with hsphfpd, as it provides the same functionality. `hardware.bluetooth.hsphfpd.enable` needs be set to false";
       }
diff --git a/nixos/modules/services/desktops/system76-scheduler.nix b/nixos/modules/services/desktops/system76-scheduler.nix
new file mode 100644
index 00000000000..267b528cc5d
--- /dev/null
+++ b/nixos/modules/services/desktops/system76-scheduler.nix
@@ -0,0 +1,296 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.system76-scheduler;
+
+  inherit (builtins) concatStringsSep map toString attrNames;
+  inherit (lib) boolToString types mkOption literalExpression mdDoc optional mkIf mkMerge;
+  inherit (types) nullOr listOf bool int ints float str enum;
+
+  withDefaults = optionSpecs: defaults:
+    lib.genAttrs (attrNames optionSpecs) (name:
+      mkOption (optionSpecs.${name} // {
+        default = optionSpecs.${name}.default or defaults.${name} or null;
+      }));
+
+  latencyProfile = withDefaults {
+    latency = {
+      type = int;
+      description = mdDoc "`sched_latency_ns`.";
+    };
+    nr-latency = {
+      type = int;
+      description = mdDoc "`sched_nr_latency`.";
+    };
+    wakeup-granularity = {
+      type = float;
+      description = mdDoc "`sched_wakeup_granularity_ns`.";
+    };
+    bandwidth-size = {
+      type = int;
+      description = mdDoc "`sched_cfs_bandwidth_slice_us`.";
+    };
+    preempt = {
+      type = enum [ "none" "voluntary" "full" ];
+      description = mdDoc "Preemption mode.";
+    };
+  };
+  schedulerProfile = withDefaults {
+    nice = {
+      type = nullOr (ints.between (-20) 19);
+      description = mdDoc "Niceness.";
+    };
+    class = {
+      type = nullOr (enum [ "idle" "batch" "other" "rr" "fifo" ]);
+      example = literalExpression "\"batch\"";
+      description = mdDoc "CPU scheduler class.";
+    };
+    prio = {
+      type = nullOr (ints.between 1 99);
+      example = literalExpression "49";
+      description = mdDoc "CPU scheduler priority.";
+    };
+    ioClass = {
+      type = nullOr (enum [ "idle" "best-effort" "realtime" ]);
+      example = literalExpression "\"best-effort\"";
+      description = mdDoc "IO scheduler class.";
+    };
+    ioPrio = {
+      type = nullOr (ints.between 0 7);
+      example = literalExpression "4";
+      description = mdDoc "IO scheduler priority.";
+    };
+    matchers = {
+      type = nullOr (listOf str);
+      default = [];
+      example = literalExpression ''
+        [
+          "include cgroup=\"/user.slice/*.service\" parent=\"systemd\""
+          "emacs"
+        ]
+      '';
+      description = mdDoc "Process matchers.";
+    };
+  };
+
+  cfsProfileToString = name: let
+    p = cfg.settings.cfsProfiles.${name};
+  in
+    "${name} latency=${toString p.latency} nr-latency=${toString p.nr-latency} wakeup-granularity=${toString p.wakeup-granularity} bandwidth-size=${toString p.bandwidth-size} preempt=\"${p.preempt}\"";
+
+  prioToString = class: prio: if prio == null then "\"${class}\"" else "(${class})${toString prio}";
+
+  schedulerProfileToString = name: a: indent:
+    concatStringsSep " "
+      (["${indent}${name}"]
+       ++ (optional (a.nice != null) "nice=${toString a.nice}")
+       ++ (optional (a.class != null) "sched=${prioToString a.class a.prio}")
+       ++ (optional (a.ioClass != null) "io=${prioToString a.ioClass a.ioPrio}")
+       ++ (optional ((builtins.length a.matchers) != 0) ("{\n${concatStringsSep "\n" (map (m: "  ${indent}${m}") a.matchers)}\n${indent}}")));
+
+in {
+  options = {
+    services.system76-scheduler = {
+      enable = lib.mkEnableOption (lib.mdDoc "system76-scheduler");
+
+      package = mkOption {
+        type = types.package;
+        default = config.boot.kernelPackages.system76-scheduler;
+        defaultText = literalExpression "config.boot.kernelPackages.system76-scheduler";
+        description = mdDoc "Which System76-Scheduler package to use.";
+      };
+
+      useStockConfig = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Use the (reasonable and featureful) stock configuration.
+
+          When this option is `true`, `services.system76-scheduler.settings`
+          are ignored.
+        '';
+      };
+
+      settings = {
+        cfsProfiles = {
+          enable = mkOption {
+            type = bool;
+            default = true;
+            description = mdDoc "Tweak CFS latency parameters when going on/off battery";
+          };
+
+          default = latencyProfile {
+            latency = 6;
+            nr-latency = 8;
+            wakeup-granularity = 1.0;
+            bandwidth-size = 5;
+            preempt = "voluntary";
+          };
+          responsive = latencyProfile {
+            latency = 4;
+            nr-latency = 10;
+            wakeup-granularity = 0.5;
+            bandwidth-size = 3;
+            preempt = "full";
+          };
+        };
+
+        processScheduler = {
+          enable = mkOption {
+            type = bool;
+            default = true;
+            description = mdDoc "Tweak scheduling of individual processes in real time.";
+          };
+
+          useExecsnoop = mkOption {
+            type = bool;
+            default = true;
+            description = mdDoc "Use execsnoop (otherwise poll the precess list periodically).";
+          };
+
+          refreshInterval = mkOption {
+            type = int;
+            default = 60;
+            description = mdDoc "Process list poll interval, in seconds";
+          };
+
+          foregroundBoost = {
+            enable = mkOption {
+              type = bool;
+              default = true;
+              description = mdDoc ''
+                Boost foreground process priorities.
+
+                (And de-boost background ones).  Note that this option needs cooperation
+                from the desktop environment to work.  On Gnome the client side is
+                implemented by the "System76 Scheduler" shell extension.
+              '';
+            };
+            foreground = schedulerProfile {
+              nice = 0;
+              ioClass = "best-effort";
+              ioPrio = 0;
+            };
+            background = schedulerProfile {
+              nice = 6;
+              ioClass = "idle";
+            };
+          };
+
+          pipewireBoost = {
+            enable = mkOption {
+              type = bool;
+              default = true;
+              description = mdDoc "Boost Pipewire client priorities.";
+            };
+            profile = schedulerProfile {
+              nice = -6;
+              ioClass = "best-effort";
+              ioPrio = 0;
+            };
+          };
+        };
+      };
+
+      assignments = mkOption {
+        type = types.attrsOf (types.submodule {
+          options = schedulerProfile { };
+        });
+        default = {};
+        example = literalExpression ''
+          {
+            nix-builds = {
+              nice = 15;
+              class = "batch";
+              ioClass = "idle";
+              matchers = [
+                "nix-daemon"
+              ];
+            };
+          }
+        '';
+        description = mdDoc "Process profile assignments.";
+      };
+
+      exceptions = mkOption {
+        type = types.listOf str;
+        default = [];
+        example = literalExpression ''
+          [
+            "include descends=\"schedtool\""
+            "schedtool"
+          ]
+        '';
+        description = mdDoc "Processes that are left alone.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+    services.dbus.packages = [ cfg.package ];
+
+    systemd.services.system76-scheduler = {
+      description = "Manage process priorities and CFS scheduler latencies for improved responsiveness on the desktop";
+      wantedBy = [ "multi-user.target" ];
+      path = [
+        # execsnoop needs those to extract kernel headers:
+        pkgs.kmod
+        pkgs.gnutar
+        pkgs.xz
+      ];
+      serviceConfig = {
+        Type = "dbus";
+        BusName= "com.system76.Scheduler";
+        ExecStart = "${cfg.package}/bin/system76-scheduler daemon";
+        ExecReload = "${cfg.package}/bin/system76-scheduler daemon reload";
+      };
+    };
+
+    environment.etc = mkMerge [
+      (mkIf cfg.useStockConfig {
+        # No custom settings: just use stock configuration with a fix for Pipewire
+        "system76-scheduler/config.kdl".source = "${cfg.package}/data/config.kdl";
+        "system76-scheduler/process-scheduler/00-dist.kdl".source = "${cfg.package}/data/pop_os.kdl";
+        "system76-scheduler/process-scheduler/01-fix-pipewire-paths.kdl".source = ../../../../pkgs/os-specific/linux/system76-scheduler/01-fix-pipewire-paths.kdl;
+      })
+
+      (let
+        settings = cfg.settings;
+        cfsp = settings.cfsProfiles;
+        ps = settings.processScheduler;
+      in mkIf (!cfg.useStockConfig) {
+        "system76-scheduler/config.kdl".text = ''
+          version "2.0"
+          autogroup-enabled false
+          cfs-profiles enable=${boolToString cfsp.enable} {
+            ${cfsProfileToString "default"}
+            ${cfsProfileToString "responsive"}
+          }
+          process-scheduler enable=${boolToString ps.enable} {
+            execsnoop ${boolToString ps.useExecsnoop}
+            refresh-rate ${toString ps.refreshInterval}
+            assignments {
+              ${if ps.foregroundBoost.enable then (schedulerProfileToString "foreground" ps.foregroundBoost.foreground "    ") else ""}
+              ${if ps.foregroundBoost.enable then (schedulerProfileToString "background" ps.foregroundBoost.background "    ") else ""}
+              ${if ps.pipewireBoost.enable then (schedulerProfileToString "pipewire" ps.pipewireBoost.profile "    ") else ""}
+            }
+          }
+        '';
+      })
+
+      {
+        "system76-scheduler/process-scheduler/02-config.kdl".text =
+          "exceptions {\n${concatStringsSep "\n" (map (e: "  ${e}") cfg.exceptions)}\n}\n"
+          + "assignments {\n"
+          + (concatStringsSep "\n" (map (name: schedulerProfileToString name cfg.assignments.${name} "  ")
+            (attrNames cfg.assignments)))
+          + "\n}\n";
+      }
+    ];
+  };
+
+  meta = {
+    maintainers = [ lib.maintainers.cmm ];
+  };
+}
diff --git a/nixos/modules/services/development/blackfire.md b/nixos/modules/services/development/blackfire.md
new file mode 100644
index 00000000000..e2e7e4780c7
--- /dev/null
+++ b/nixos/modules/services/development/blackfire.md
@@ -0,0 +1,39 @@
+# Blackfire profiler {#module-services-blackfire}
+
+*Source:* {file}`modules/services/development/blackfire.nix`
+
+*Upstream documentation:* <https://blackfire.io/docs/introduction>
+
+[Blackfire](https://blackfire.io) is a proprietary tool for profiling applications. There are several languages supported by the product but currently only PHP support is packaged in Nixpkgs. The back-end consists of a module that is loaded into the language runtime (called *probe*) and a service (*agent*) that the probe connects to and that sends the profiles to the server.
+
+To use it, you will need to enable the agent and the probe on your server. The exact method will depend on the way you use PHP but here is an example of NixOS configuration for PHP-FPM:
+```
+let
+  php = pkgs.php.withExtensions ({ enabled, all }: enabled ++ (with all; [
+    blackfire
+  ]));
+in {
+  # Enable the probe extension for PHP-FPM.
+  services.phpfpm = {
+    phpPackage = php;
+  };
+
+  # Enable and configure the agent.
+  services.blackfire-agent = {
+    enable = true;
+    settings = {
+      # You will need to get credentials at https://blackfire.io/my/settings/credentials
+      # You can also use other options described in https://blackfire.io/docs/up-and-running/configuration/agent
+      server-id = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
+      server-token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
+    };
+  };
+
+  # Make the agent run on start-up.
+  # (WantedBy= from the upstream unit not respected: https://github.com/NixOS/nixpkgs/issues/81138)
+  # Alternately, you can start it manually with `systemctl start blackfire-agent`.
+  systemd.services.blackfire-agent.wantedBy = [ "phpfpm-foo.service" ];
+}
+```
+
+On your developer machine, you will also want to install [the client](https://blackfire.io/docs/up-and-running/installation#install-a-profiling-client) (see `blackfire` package) or the browser extension to actually trigger the profiling.
diff --git a/nixos/modules/services/development/blackfire.nix b/nixos/modules/services/development/blackfire.nix
index 054cef9ae80..3c98d7a281c 100644
--- a/nixos/modules/services/development/blackfire.nix
+++ b/nixos/modules/services/development/blackfire.nix
@@ -11,7 +11,7 @@ let
 in {
   meta = {
     maintainers = pkgs.blackfire.meta.maintainers;
-    doc = ./blackfire.xml;
+    doc = ./blackfire.md;
   };
 
   options = {
diff --git a/nixos/modules/services/development/blackfire.xml b/nixos/modules/services/development/blackfire.xml
deleted file mode 100644
index cecd249dda4..00000000000
--- a/nixos/modules/services/development/blackfire.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" version="5.0" xml:id="module-services-blackfire">
- <title>Blackfire profiler</title>
- <para>
-  <emphasis>Source:</emphasis>
-  <filename>modules/services/development/blackfire.nix</filename>
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://blackfire.io/docs/introduction"/>
- </para>
- <para>
-  <link xlink:href="https://blackfire.io">Blackfire</link> is a proprietary tool for profiling applications. There are several languages supported by the product but currently only PHP support is packaged in Nixpkgs. The back-end consists of a module that is loaded into the language runtime (called <firstterm>probe</firstterm>) and a service (<firstterm>agent</firstterm>) that the probe connects to and that sends the profiles to the server.
- </para>
- <para>
-  To use it, you will need to enable the agent and the probe on your server. The exact method will depend on the way you use PHP but here is an example of NixOS configuration for PHP-FPM:
-<programlisting>let
-  php = pkgs.php.withExtensions ({ enabled, all }: enabled ++ (with all; [
-    blackfire
-  ]));
-in {
-  # Enable the probe extension for PHP-FPM.
-  services.phpfpm = {
-    phpPackage = php;
-  };
-
-  # Enable and configure the agent.
-  services.blackfire-agent = {
-    enable = true;
-    settings = {
-      # You will need to get credentials at https://blackfire.io/my/settings/credentials
-      # You can also use other options described in https://blackfire.io/docs/up-and-running/configuration/agent
-      server-id = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
-      server-token = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
-    };
-  };
-
-  # Make the agent run on start-up.
-  # (WantedBy= from the upstream unit not respected: https://github.com/NixOS/nixpkgs/issues/81138)
-  # Alternately, you can start it manually with `systemctl start blackfire-agent`.
-  systemd.services.blackfire-agent.wantedBy = [ "phpfpm-foo.service" ];
-}</programlisting>
- </para>
- <para>
-  On your developer machine, you will also want to install <link xlink:href="https://blackfire.io/docs/up-and-running/installation#install-a-profiling-client">the client</link> (see <package>blackfire</package> package) or the browser extension to actually trigger the profiling.
- </para>
-</chapter>
diff --git a/nixos/modules/services/development/gemstash.nix b/nixos/modules/services/development/gemstash.nix
new file mode 100644
index 00000000000..eb7ccb98bde
--- /dev/null
+++ b/nixos/modules/services/development/gemstash.nix
@@ -0,0 +1,103 @@
+{ lib, pkgs, config, ... }:
+with lib;
+
+let
+  settingsFormat = pkgs.formats.yaml { };
+
+  # gemstash uses a yaml config where the keys are ruby symbols,
+  # which means they start with ':'. This would be annoying to use
+  # on the nix side, so we rewrite plain names instead.
+  prefixColon = s: listToAttrs (map
+    (attrName: {
+      name = ":${attrName}";
+      value =
+        if isAttrs s.${attrName}
+        then prefixColon s."${attrName}"
+        else s."${attrName}";
+    })
+    (attrNames s));
+
+  # parse the port number out of the tcp://ip:port bind setting string
+  parseBindPort = bind: strings.toInt (last (strings.splitString ":" bind));
+
+  cfg = config.services.gemstash;
+in
+{
+  options.services.gemstash = {
+    enable = mkEnableOption (lib.mdDoc "gemstash service");
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to open the firewall for the port in {option}`services.gemstash.bind`.
+      '';
+    };
+
+    settings = mkOption {
+      default = {};
+      description = lib.mdDoc ''
+        Configuration for Gemstash. The details can be found at in
+        [gemstash documentation](https://github.com/rubygems/gemstash/blob/master/man/gemstash-configuration.5.md).
+        Each key set here is automatically prefixed with ":" to match the gemstash expectations.
+      '';
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+        options = {
+          base_path = mkOption {
+            type = types.path;
+            default = "/var/lib/gemstash";
+            description = lib.mdDoc "Path to store the gem files and the sqlite database. If left unchanged, the directory will be created.";
+          };
+          bind = mkOption {
+            type = types.str;
+            default = "tcp://0.0.0.0:9292";
+            description = lib.mdDoc "Host and port combination for the server to listen on.";
+          };
+          db_adapter = mkOption {
+            type = types.nullOr (types.enum [ "sqlite3" "postgres" "mysql" "mysql2" ]);
+            default = null;
+            description = lib.mdDoc "Which database type to use. For choices other than sqlite3, the dbUrl has to be specified as well.";
+          };
+          db_url = mkOption {
+            type = types.nullOr types.str;
+            default = null;
+            description = lib.mdDoc "The database to connect to when using postgres, mysql, or mysql2.";
+          };
+        };
+      };
+    };
+  };
+
+  config =
+    mkIf cfg.enable {
+      users = {
+        users.gemstash = {
+          group = "gemstash";
+          isSystemUser = true;
+        };
+        groups.gemstash = { };
+      };
+
+      networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ (parseBindPort cfg.settings.bind) ];
+
+      systemd.services.gemstash = {
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        serviceConfig = mkMerge [
+          {
+            ExecStart = "${pkgs.gemstash}/bin/gemstash start --no-daemonize --config-file ${settingsFormat.generate "gemstash.yaml" (prefixColon cfg.settings)}";
+            NoNewPrivileges = true;
+            User = "gemstash";
+            Group = "gemstash";
+            PrivateTmp = true;
+            RestrictSUIDSGID = true;
+            LockPersonality = true;
+          }
+          (mkIf (cfg.settings.base_path == "/var/lib/gemstash") {
+            StateDirectory = "gemstash";
+          })
+        ];
+      };
+    };
+}
diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix
index 8c64e3d9a56..74f56f5890f 100644
--- a/nixos/modules/services/development/lorri.nix
+++ b/nixos/modules/services/development/lorri.nix
@@ -50,6 +50,6 @@ in {
       };
     };
 
-    environment.systemPackages = [ cfg.package ];
+    environment.systemPackages = [ cfg.package pkgs.direnv ];
   };
 }
diff --git a/nixos/modules/services/development/zammad.nix b/nixos/modules/services/development/zammad.nix
index 7de11b08b7e..0faeb4c0e9f 100644
--- a/nixos/modules/services/development/zammad.nix
+++ b/nixos/modules/services/development/zammad.nix
@@ -28,7 +28,7 @@ in
 
   options = {
     services.zammad = {
-      enable = mkEnableOption (lib.mdDoc "Zammad, a web-based, open source user support/ticketing solution.");
+      enable = mkEnableOption (lib.mdDoc "Zammad, a web-based, open source user support/ticketing solution");
 
       package = mkOption {
         type = types.package;
diff --git a/nixos/modules/services/display-managers/greetd.nix b/nixos/modules/services/display-managers/greetd.nix
index 96eddc1ce7c..3a0f59f62af 100644
--- a/nixos/modules/services/display-managers/greetd.nix
+++ b/nixos/modules/services/display-managers/greetd.nix
@@ -89,6 +89,8 @@ in
         SendSIGHUP = true;
         TimeoutStopSec = "30s";
         KeyringMode = "shared";
+
+        Type = "idle";
       };
 
       # Don't kill a user session when using nixos-rebuild
diff --git a/nixos/modules/services/editors/emacs.md b/nixos/modules/services/editors/emacs.md
new file mode 100644
index 00000000000..72364b29514
--- /dev/null
+++ b/nixos/modules/services/editors/emacs.md
@@ -0,0 +1,420 @@
+# Emacs {#module-services-emacs}
+
+<!--
+    Documentation contributors:
+      Damien Cassou @DamienCassou
+      Thomas Tuegel @ttuegel
+      Rodney Lorrimar @rvl
+      Adam Hoese @adisbladis
+  -->
+
+[Emacs](https://www.gnu.org/software/emacs/) is an
+extensible, customizable, self-documenting real-time display editor — and
+more. At its core is an interpreter for Emacs Lisp, a dialect of the Lisp
+programming language with extensions to support text editing.
+
+Emacs runs within a graphical desktop environment using the X Window System,
+but works equally well on a text terminal. Under
+macOS, a "Mac port" edition is available, which
+uses Apple's native GUI frameworks.
+
+Nixpkgs provides a superior environment for
+running Emacs. It's simple to create custom builds
+by overriding the default packages. Chaotic collections of Emacs Lisp code
+and extensions can be brought under control using declarative package
+management. NixOS even provides a
+{command}`systemd` user service for automatically starting the Emacs
+daemon.
+
+## Installing Emacs {#module-services-emacs-installing}
+
+Emacs can be installed in the normal way for Nix (see
+[](#sec-package-management)). In addition, a NixOS
+*service* can be enabled.
+
+### The Different Releases of Emacs {#module-services-emacs-releases}
+
+Nixpkgs defines several basic Emacs packages.
+The following are attributes belonging to the {var}`pkgs` set:
+
+  {var}`emacs`
+  : The latest stable version of Emacs using the [GTK 2](http://www.gtk.org)
+    widget toolkit.
+
+  {var}`emacs-nox`
+  : Emacs built without any dependency on X11 libraries.
+
+  {var}`emacsMacport`
+  : Emacs with the "Mac port" patches, providing a more native look and
+    feel under macOS.
+
+If those aren't suitable, then the following imitation Emacs editors are
+also available in Nixpkgs:
+[Zile](https://www.gnu.org/software/zile/),
+[mg](http://homepage.boetes.org/software/mg/),
+[Yi](http://yi-editor.github.io/),
+[jmacs](https://joe-editor.sourceforge.io/).
+
+### Adding Packages to Emacs {#module-services-emacs-adding-packages}
+
+Emacs includes an entire ecosystem of functionality beyond text editing,
+including a project planner, mail and news reader, debugger interface,
+calendar, and more.
+
+Most extensions are gotten with the Emacs packaging system
+({file}`package.el`) from
+[Emacs Lisp Package Archive (ELPA)](https://elpa.gnu.org/),
+[MELPA](https://melpa.org/),
+[MELPA Stable](https://stable.melpa.org/), and
+[Org ELPA](http://orgmode.org/elpa.html). Nixpkgs is
+regularly updated to mirror all these archives.
+
+Under NixOS, you can continue to use
+`package-list-packages` and
+`package-install` to install packages. You can also
+declare the set of Emacs packages you need using the derivations from
+Nixpkgs. The rest of this section discusses declarative installation of
+Emacs packages through nixpkgs.
+
+The first step to declare the list of packages you want in your Emacs
+installation is to create a dedicated derivation. This can be done in a
+dedicated {file}`emacs.nix` file such as:
+
+::: {.example #ex-emacsNix}
+### Nix expression to build Emacs with packages (`emacs.nix`)
+
+```nix
+/*
+This is a nix expression to build Emacs and some Emacs packages I like
+from source on any distribution where Nix is installed. This will install
+all the dependencies from the nixpkgs repository and build the binary files
+without interfering with the host distribution.
+
+To build the project, type the following from the current directory:
+
+$ nix-build emacs.nix
+
+To run the newly compiled executable:
+
+$ ./result/bin/emacs
+*/
+
+# The first non-comment line in this file indicates that
+# the whole file represents a function.
+{ pkgs ? import <nixpkgs> {} }:
+
+let
+  # The let expression below defines a myEmacs binding pointing to the
+  # current stable version of Emacs. This binding is here to separate
+  # the choice of the Emacs binary from the specification of the
+  # required packages.
+  myEmacs = pkgs.emacs;
+  # This generates an emacsWithPackages function. It takes a single
+  # argument: a function from a package set to a list of packages
+  # (the packages that will be available in Emacs).
+  emacsWithPackages = (pkgs.emacsPackagesFor myEmacs).emacsWithPackages;
+in
+  # The rest of the file specifies the list of packages to install. In the
+  # example, two packages (magit and zerodark-theme) are taken from
+  # MELPA stable.
+  emacsWithPackages (epkgs: (with epkgs.melpaStablePackages; [
+    magit          # ; Integrate git <C-x g>
+    zerodark-theme # ; Nicolas' theme
+  ])
+  # Two packages (undo-tree and zoom-frm) are taken from MELPA.
+  ++ (with epkgs.melpaPackages; [
+    undo-tree      # ; <C-x u> to show the undo tree
+    zoom-frm       # ; increase/decrease font size for all buffers %lt;C-x C-+>
+  ])
+  # Three packages are taken from GNU ELPA.
+  ++ (with epkgs.elpaPackages; [
+    auctex         # ; LaTeX mode
+    beacon         # ; highlight my cursor when scrolling
+    nameless       # ; hide current package name everywhere in elisp code
+  ])
+  # notmuch is taken from a nixpkgs derivation which contains an Emacs mode.
+  ++ [
+    pkgs.notmuch   # From main packages set
+  ])
+```
+:::
+
+The result of this configuration will be an {command}`emacs`
+command which launches Emacs with all of your chosen packages in the
+{var}`load-path`.
+
+You can check that it works by executing this in a terminal:
+```ShellSession
+$ nix-build emacs.nix
+$ ./result/bin/emacs -q
+```
+and then typing `M-x package-initialize`. Check that you
+can use all the packages you want in this Emacs instance. For example, try
+switching to the zerodark theme through `M-x load-theme <RET> zerodark <RET> y`.
+
+::: {.tip}
+A few popular extensions worth checking out are: auctex, company,
+edit-server, flycheck, helm, iedit, magit, multiple-cursors, projectile,
+and yasnippet.
+:::
+
+The list of available packages in the various ELPA repositories can be seen
+with the following commands:
+::: {.example #module-services-emacs-querying-packages}
+### Querying Emacs packages
+
+```
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.elpaPackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaPackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaStablePackages
+nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.orgPackages
+```
+:::
+
+If you are on NixOS, you can install this particular Emacs for all users by
+adding it to the list of system packages (see
+[](#sec-declarative-package-mgmt)). Simply modify your file
+{file}`configuration.nix` to make it contain:
+::: {.example #module-services-emacs-configuration-nix}
+### Custom Emacs in `configuration.nix`
+
+```
+{
+ environment.systemPackages = [
+   # [...]
+   (import /path/to/emacs.nix { inherit pkgs; })
+  ];
+}
+```
+:::
+
+In this case, the next {command}`nixos-rebuild switch` will take
+care of adding your {command}`emacs` to the {var}`PATH`
+environment variable (see [](#sec-changing-config)).
+
+<!-- fixme: i think the following is better done with config.nix
+https://nixos.org/nixpkgs/manual/#sec-modify-via-packageOverrides
+-->
+
+If you are not on NixOS or want to install this particular Emacs only for
+yourself, you can do so by adding it to your
+{file}`~/.config/nixpkgs/config.nix` (see
+[Nixpkgs manual](https://nixos.org/nixpkgs/manual/#sec-modify-via-packageOverrides)):
+::: {.example #module-services-emacs-config-nix}
+### Custom Emacs in `~/.config/nixpkgs/config.nix`
+
+```
+{
+  packageOverrides = super: let self = super.pkgs; in {
+    myemacs = import /path/to/emacs.nix { pkgs = self; };
+  };
+}
+```
+:::
+
+In this case, the next `nix-env -f '<nixpkgs>' -iA
+myemacs` will take care of adding your emacs to the
+{var}`PATH` environment variable.
+
+### Advanced Emacs Configuration {#module-services-emacs-advanced}
+
+If you want, you can tweak the Emacs package itself from your
+{file}`emacs.nix`. For example, if you want to have a
+GTK 3-based Emacs instead of the default GTK 2-based binary and remove the
+automatically generated {file}`emacs.desktop` (useful if you
+only use {command}`emacsclient`), you can change your file
+{file}`emacs.nix` in this way:
+
+::: {.example #ex-emacsGtk3Nix}
+### Custom Emacs build
+
+```
+{ pkgs ? import <nixpkgs> {} }:
+let
+  myEmacs = (pkgs.emacs.override {
+    # Use gtk3 instead of the default gtk2
+    withGTK3 = true;
+    withGTK2 = false;
+  }).overrideAttrs (attrs: {
+    # I don't want emacs.desktop file because I only use
+    # emacsclient.
+    postInstall = (attrs.postInstall or "") + ''
+      rm $out/share/applications/emacs.desktop
+    '';
+  });
+in [...]
+```
+:::
+
+After building this file as shown in [](#ex-emacsNix), you
+will get an GTK 3-based Emacs binary pre-loaded with your favorite packages.
+
+## Running Emacs as a Service {#module-services-emacs-running}
+
+NixOS provides an optional
+{command}`systemd` service which launches
+[Emacs daemon](https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html)
+with the user's login session.
+
+*Source:* {file}`modules/services/editors/emacs.nix`
+
+### Enabling the Service {#module-services-emacs-enabling}
+
+To install and enable the {command}`systemd` user service for Emacs
+daemon, add the following to your {file}`configuration.nix`:
+```
+services.emacs.enable = true;
+services.emacs.package = import /home/cassou/.emacs.d { pkgs = pkgs; };
+```
+
+The {var}`services.emacs.package` option allows a custom
+derivation to be used, for example, one created by
+`emacsWithPackages`.
+
+Ensure that the Emacs server is enabled for your user's Emacs
+configuration, either by customizing the {var}`server-mode`
+variable, or by adding `(server-start)` to
+{file}`~/.emacs.d/init.el`.
+
+To start the daemon, execute the following:
+```ShellSession
+$ nixos-rebuild switch  # to activate the new configuration.nix
+$ systemctl --user daemon-reload        # to force systemd reload
+$ systemctl --user start emacs.service  # to start the Emacs daemon
+```
+The server should now be ready to serve Emacs clients.
+
+### Starting the client {#module-services-emacs-starting-client}
+
+Ensure that the emacs server is enabled, either by customizing the
+{var}`server-mode` variable, or by adding
+`(server-start)` to {file}`~/.emacs`.
+
+To connect to the emacs daemon, run one of the following:
+```
+emacsclient FILENAME
+emacsclient --create-frame  # opens a new frame (window)
+emacsclient --create-frame --tty  # opens a new frame on the current terminal
+```
+
+### Configuring the {var}`EDITOR` variable {#module-services-emacs-editor-variable}
+
+<!--<title>{command}`emacsclient` as the Default Editor</title>-->
+
+If [](#opt-services.emacs.defaultEditor) is
+`true`, the {var}`EDITOR` variable will be set
+to a wrapper script which launches {command}`emacsclient`.
+
+Any setting of {var}`EDITOR` in the shell config files will
+override {var}`services.emacs.defaultEditor`. To make sure
+{var}`EDITOR` refers to the Emacs wrapper script, remove any
+existing {var}`EDITOR` assignment from
+{file}`.profile`, {file}`.bashrc`,
+{file}`.zshenv` or any other shell config file.
+
+If you have formed certain bad habits when editing files, these can be
+corrected with a shell alias to the wrapper script:
+```
+alias vi=$EDITOR
+```
+
+### Per-User Enabling of the Service {#module-services-emacs-per-user}
+
+In general, {command}`systemd` user services are globally enabled
+by symlinks in {file}`/etc/systemd/user`. In the case where
+Emacs daemon is not wanted for all users, it is possible to install the
+service but not globally enable it:
+```
+services.emacs.enable = false;
+services.emacs.install = true;
+```
+
+To enable the {command}`systemd` user service for just the
+currently logged in user, run:
+```
+systemctl --user enable emacs
+```
+This will add the symlink
+{file}`~/.config/systemd/user/emacs.service`.
+
+## Configuring Emacs {#module-services-emacs-configuring}
+
+The Emacs init file should be changed to load the extension packages at
+startup:
+
+::: {.example #module-services-emacs-package-initialisation}
+### Package initialization in `.emacs`
+
+```
+(require 'package)
+
+;; optional. makes unpure packages archives unavailable
+(setq package-archives nil)
+
+(setq package-enable-at-startup nil)
+(package-initialize)
+```
+:::
+
+After the declarative emacs package configuration has been tested,
+previously downloaded packages can be cleaned up by removing
+{file}`~/.emacs.d/elpa` (do make a backup first, in case you
+forgot a package).
+
+<!--
+      todo: is it worth documenting customizations for
+      server-switch-hook, server-done-hook?
+  -->
+
+### A Major Mode for Nix Expressions {#module-services-emacs-major-mode}
+
+Of interest may be {var}`melpaPackages.nix-mode`, which
+provides syntax highlighting for the Nix language. This is particularly
+convenient if you regularly edit Nix files.
+
+### Accessing man pages {#module-services-emacs-man-pages}
+
+You can use `woman` to get completion of all available
+man pages. For example, type `M-x woman <RET> nixos-rebuild <RET>.`
+
+### Editing DocBook 5 XML Documents {#sec-emacs-docbook-xml}
+
+Emacs includes
+[nXML](https://www.gnu.org/software/emacs/manual/html_node/nxml-mode/Introduction.html),
+a major-mode for validating and editing XML documents. When editing DocBook
+5.0 documents, such as [this one](#book-nixos-manual),
+nXML needs to be configured with the relevant schema, which is not
+included.
+
+To install the DocBook 5.0 schemas, either add
+{var}`pkgs.docbook5` to [](#opt-environment.systemPackages)
+([NixOS](#sec-declarative-package-mgmt)), or run
+`nix-env -f '<nixpkgs>' -iA docbook5`
+([Nix](#sec-ad-hoc-packages)).
+
+Then customize the variable {var}`rng-schema-locating-files` to
+include {file}`~/.emacs.d/schemas.xml` and put the following
+text into that file:
+::: {.example #ex-emacs-docbook-xml}
+### nXML Schema Configuration (`~/.emacs.d/schemas.xml`)
+
+```xml
+<?xml version="1.0"?>
+<!--
+  To let emacs find this file, evaluate:
+  (add-to-list 'rng-schema-locating-files "~/.emacs.d/schemas.xml")
+-->
+<locatingRules xmlns="http://thaiopensource.com/ns/locating-rules/1.0">
+  <!--
+    Use this variation if pkgs.docbook5 is added to environment.systemPackages
+  -->
+  <namespace ns="http://docbook.org/ns/docbook"
+             uri="/run/current-system/sw/share/xml/docbook-5.0/rng/docbookxi.rnc"/>
+  <!--
+    Use this variation if installing schema with "nix-env -iA pkgs.docbook5".
+  <namespace ns="http://docbook.org/ns/docbook"
+             uri="../.nix-profile/share/xml/docbook-5.0/rng/docbookxi.rnc"/>
+  -->
+</locatingRules>
+```
+:::
diff --git a/nixos/modules/services/editors/emacs.nix b/nixos/modules/services/editors/emacs.nix
index 5ae28cd9bbb..2be46e47d64 100644
--- a/nixos/modules/services/editors/emacs.nix
+++ b/nixos/modules/services/editors/emacs.nix
@@ -99,5 +99,5 @@ in
     environment.variables.EDITOR = mkIf cfg.defaultEditor (mkOverride 900 "${editorScript}/bin/emacseditor");
   };
 
-  meta.doc = ./emacs.xml;
+  meta.doc = ./emacs.md;
 }
diff --git a/nixos/modules/services/editors/emacs.xml b/nixos/modules/services/editors/emacs.xml
deleted file mode 100644
index fd99ee9442c..00000000000
--- a/nixos/modules/services/editors/emacs.xml
+++ /dev/null
@@ -1,580 +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-emacs">
- <title>Emacs</title>
-<!--
-    Documentation contributors:
-      Damien Cassou @DamienCassou
-      Thomas Tuegel @ttuegel
-      Rodney Lorrimar @rvl
-      Adam Hoese @adisbladis
-  -->
- <para>
-  <link xlink:href="https://www.gnu.org/software/emacs/">Emacs</link> is an
-  extensible, customizable, self-documenting real-time display editor — and
-  more. At its core is an interpreter for Emacs Lisp, a dialect of the Lisp
-  programming language with extensions to support text editing.
- </para>
- <para>
-  Emacs runs within a graphical desktop environment using the X Window System,
-  but works equally well on a text terminal. Under
-  <productname>macOS</productname>, a "Mac port" edition is available, which
-  uses Apple's native GUI frameworks.
- </para>
- <para>
-  <productname>Nixpkgs</productname> provides a superior environment for
-  running <application>Emacs</application>. It's simple to create custom builds
-  by overriding the default packages. Chaotic collections of Emacs Lisp code
-  and extensions can be brought under control using declarative package
-  management. <productname>NixOS</productname> even provides a
-  <command>systemd</command> user service for automatically starting the Emacs
-  daemon.
- </para>
- <section xml:id="module-services-emacs-installing">
-  <title>Installing <application>Emacs</application></title>
-
-  <para>
-   Emacs can be installed in the normal way for Nix (see
-   <xref linkend="sec-package-management" />). In addition, a NixOS
-   <emphasis>service</emphasis> can be enabled.
-  </para>
-
-  <section xml:id="module-services-emacs-releases">
-   <title>The Different Releases of Emacs</title>
-
-   <para>
-    <productname>Nixpkgs</productname> defines several basic Emacs packages.
-    The following are attributes belonging to the <varname>pkgs</varname> set:
-    <variablelist>
-     <varlistentry>
-      <term>
-       <varname>emacs</varname>
-      </term>
-      <term>
-       <varname>emacs</varname>
-      </term>
-      <listitem>
-       <para>
-        The latest stable version of Emacs using the
-        <link
-                xlink:href="http://www.gtk.org">GTK 2</link>
-        widget toolkit.
-       </para>
-      </listitem>
-     </varlistentry>
-     <varlistentry>
-      <term>
-       <varname>emacs-nox</varname>
-      </term>
-      <listitem>
-       <para>
-        Emacs built without any dependency on X11 libraries.
-       </para>
-      </listitem>
-     </varlistentry>
-     <varlistentry>
-      <term>
-       <varname>emacsMacport</varname>
-      </term>
-      <term>
-       <varname>emacsMacport</varname>
-      </term>
-      <listitem>
-       <para>
-        Emacs with the "Mac port" patches, providing a more native look and
-        feel under macOS.
-       </para>
-      </listitem>
-     </varlistentry>
-    </variablelist>
-   </para>
-
-   <para>
-    If those aren't suitable, then the following imitation Emacs editors are
-    also available in Nixpkgs:
-    <link xlink:href="https://www.gnu.org/software/zile/">Zile</link>,
-    <link xlink:href="http://homepage.boetes.org/software/mg/">mg</link>,
-    <link xlink:href="http://yi-editor.github.io/">Yi</link>,
-    <link xlink:href="https://joe-editor.sourceforge.io/">jmacs</link>.
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-adding-packages">
-   <title>Adding Packages to Emacs</title>
-
-   <para>
-    Emacs includes an entire ecosystem of functionality beyond text editing,
-    including a project planner, mail and news reader, debugger interface,
-    calendar, and more.
-   </para>
-
-   <para>
-    Most extensions are gotten with the Emacs packaging system
-    (<filename>package.el</filename>) from
-    <link
-        xlink:href="https://elpa.gnu.org/">Emacs Lisp Package Archive
-    (<acronym>ELPA</acronym>)</link>,
-    <link xlink:href="https://melpa.org/"><acronym>MELPA</acronym></link>,
-    <link xlink:href="https://stable.melpa.org/">MELPA Stable</link>, and
-    <link xlink:href="http://orgmode.org/elpa.html">Org ELPA</link>. Nixpkgs is
-    regularly updated to mirror all these archives.
-   </para>
-
-   <para>
-    Under NixOS, you can continue to use
-    <function>package-list-packages</function> and
-    <function>package-install</function> to install packages. You can also
-    declare the set of Emacs packages you need using the derivations from
-    Nixpkgs. The rest of this section discusses declarative installation of
-    Emacs packages through nixpkgs.
-   </para>
-
-   <para>
-    The first step to declare the list of packages you want in your Emacs
-    installation is to create a dedicated derivation. This can be done in a
-    dedicated <filename>emacs.nix</filename> file such as:
-    <example xml:id="ex-emacsNix">
-     <title>Nix expression to build Emacs with packages (<filename>emacs.nix</filename>)</title>
-<programlisting language="nix">
-/*
-This is a nix expression to build Emacs and some Emacs packages I like
-from source on any distribution where Nix is installed. This will install
-all the dependencies from the nixpkgs repository and build the binary files
-without interfering with the host distribution.
-
-To build the project, type the following from the current directory:
-
-$ nix-build emacs.nix
-
-To run the newly compiled executable:
-
-$ ./result/bin/emacs
-*/
-{ pkgs ? import &lt;nixpkgs&gt; {} }: <co xml:id="ex-emacsNix-1" />
-
-let
-  myEmacs = pkgs.emacs; <co xml:id="ex-emacsNix-2" />
-  emacsWithPackages = (pkgs.emacsPackagesFor myEmacs).emacsWithPackages; <co xml:id="ex-emacsNix-3" />
-in
-  emacsWithPackages (epkgs: (with epkgs.melpaStablePackages; [ <co xml:id="ex-emacsNix-4" />
-    magit          # ; Integrate git &lt;C-x g&gt;
-    zerodark-theme # ; Nicolas' theme
-  ]) ++ (with epkgs.melpaPackages; [ <co xml:id="ex-emacsNix-5" />
-    undo-tree      # ; &lt;C-x u&gt; to show the undo tree
-    zoom-frm       # ; increase/decrease font size for all buffers %lt;C-x C-+&gt;
-  ]) ++ (with epkgs.elpaPackages; [ <co xml:id="ex-emacsNix-6" />
-    auctex         # ; LaTeX mode
-    beacon         # ; highlight my cursor when scrolling
-    nameless       # ; hide current package name everywhere in elisp code
-  ]) ++ [
-    pkgs.notmuch   # From main packages set <co xml:id="ex-emacsNix-7" />
-  ])
-</programlisting>
-    </example>
-    <calloutlist>
-     <callout arearefs="ex-emacsNix-1">
-      <para>
-       The first non-comment line in this file (<literal>{ pkgs ? ...
-       }</literal>) indicates that the whole file represents a function.
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-2">
-      <para>
-       The <varname>let</varname> expression below defines a
-       <varname>myEmacs</varname> binding pointing to the current stable
-       version of Emacs. This binding is here to separate the choice of the
-       Emacs binary from the specification of the required packages.
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-3">
-      <para>
-       This generates an <varname>emacsWithPackages</varname> function. It
-       takes a single argument: a function from a package set to a list of
-       packages (the packages that will be available in Emacs).
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-4">
-      <para>
-       The rest of the file specifies the list of packages to install. In the
-       example, two packages (<varname>magit</varname> and
-       <varname>zerodark-theme</varname>) are taken from MELPA stable.
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-5">
-      <para>
-       Two packages (<varname>undo-tree</varname> and
-       <varname>zoom-frm</varname>) are taken from MELPA.
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-6">
-      <para>
-       Three packages are taken from GNU ELPA.
-      </para>
-     </callout>
-     <callout arearefs="ex-emacsNix-7">
-      <para>
-       <varname>notmuch</varname> is taken from a nixpkgs derivation which
-       contains an Emacs mode.
-      </para>
-     </callout>
-    </calloutlist>
-   </para>
-
-   <para>
-    The result of this configuration will be an <command>emacs</command>
-    command which launches Emacs with all of your chosen packages in the
-    <varname>load-path</varname>.
-   </para>
-
-   <para>
-    You can check that it works by executing this in a terminal:
-<screen>
-<prompt>$ </prompt>nix-build emacs.nix
-<prompt>$ </prompt>./result/bin/emacs -q
-</screen>
-    and then typing <literal>M-x package-initialize</literal>. Check that you
-    can use all the packages you want in this Emacs instance. For example, try
-    switching to the zerodark theme through <literal>M-x load-theme &lt;RET&gt;
-    zerodark &lt;RET&gt; y</literal>.
-   </para>
-
-   <tip>
-    <para>
-     A few popular extensions worth checking out are: auctex, company,
-     edit-server, flycheck, helm, iedit, magit, multiple-cursors, projectile,
-     and yasnippet.
-    </para>
-   </tip>
-
-   <para>
-    The list of available packages in the various ELPA repositories can be seen
-    with the following commands:
-    <example xml:id="module-services-emacs-querying-packages">
-     <title>Querying Emacs packages</title>
-<programlisting><![CDATA[
-nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.elpaPackages
-nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaPackages
-nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.melpaStablePackages
-nix-env -f "<nixpkgs>" -qaP -A emacs.pkgs.orgPackages
-]]></programlisting>
-    </example>
-   </para>
-
-   <para>
-    If you are on NixOS, you can install this particular Emacs for all users by
-    adding it to the list of system packages (see
-    <xref linkend="sec-declarative-package-mgmt" />). Simply modify your file
-    <filename>configuration.nix</filename> to make it contain:
-    <example xml:id="module-services-emacs-configuration-nix">
-     <title>Custom Emacs in <filename>configuration.nix</filename></title>
-<programlisting><![CDATA[
-{
- environment.systemPackages = [
-   # [...]
-   (import /path/to/emacs.nix { inherit pkgs; })
-  ];
-}
-]]></programlisting>
-    </example>
-   </para>
-
-   <para>
-    In this case, the next <command>nixos-rebuild switch</command> will take
-    care of adding your <command>emacs</command> to the <varname>PATH</varname>
-    environment variable (see <xref linkend="sec-changing-config" />).
-   </para>
-
-<!-- fixme: i think the following is better done with config.nix
-https://nixos.org/nixpkgs/manual/#sec-modify-via-packageOverrides
--->
-
-   <para>
-    If you are not on NixOS or want to install this particular Emacs only for
-    yourself, you can do so by adding it to your
-    <filename>~/.config/nixpkgs/config.nix</filename> (see
-    <link xlink:href="https://nixos.org/nixpkgs/manual/#sec-modify-via-packageOverrides">Nixpkgs
-    manual</link>):
-    <example xml:id="module-services-emacs-config-nix">
-     <title>Custom Emacs in <filename>~/.config/nixpkgs/config.nix</filename></title>
-<programlisting><![CDATA[
-{
-  packageOverrides = super: let self = super.pkgs; in {
-    myemacs = import /path/to/emacs.nix { pkgs = self; };
-  };
-}
-]]></programlisting>
-    </example>
-   </para>
-
-   <para>
-    In this case, the next <literal>nix-env -f '&lt;nixpkgs&gt;' -iA
-    myemacs</literal> will take care of adding your emacs to the
-    <varname>PATH</varname> environment variable.
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-advanced">
-   <title>Advanced Emacs Configuration</title>
-
-   <para>
-    If you want, you can tweak the Emacs package itself from your
-    <filename>emacs.nix</filename>. For example, if you want to have a
-    GTK 3-based Emacs instead of the default GTK 2-based binary and remove the
-    automatically generated <filename>emacs.desktop</filename> (useful if you
-    only use <command>emacsclient</command>), you can change your file
-    <filename>emacs.nix</filename> in this way:
-   </para>
-
-   <example xml:id="ex-emacsGtk3Nix">
-    <title>Custom Emacs build</title>
-<programlisting><![CDATA[
-{ pkgs ? import <nixpkgs> {} }:
-let
-  myEmacs = (pkgs.emacs.override {
-    # Use gtk3 instead of the default gtk2
-    withGTK3 = true;
-    withGTK2 = false;
-  }).overrideAttrs (attrs: {
-    # I don't want emacs.desktop file because I only use
-    # emacsclient.
-    postInstall = (attrs.postInstall or "") + ''
-      rm $out/share/applications/emacs.desktop
-    '';
-  });
-in [...]
-]]></programlisting>
-   </example>
-
-   <para>
-    After building this file as shown in <xref linkend="ex-emacsNix" />, you
-    will get an GTK 3-based Emacs binary pre-loaded with your favorite packages.
-   </para>
-  </section>
- </section>
- <section xml:id="module-services-emacs-running">
-  <title>Running Emacs as a Service</title>
-
-  <para>
-   <productname>NixOS</productname> provides an optional
-   <command>systemd</command> service which launches
-   <link xlink:href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Emacs-Server.html">
-   Emacs daemon </link> with the user's login session.
-  </para>
-
-  <para>
-   <emphasis>Source:</emphasis>
-   <filename>modules/services/editors/emacs.nix</filename>
-  </para>
-
-  <section xml:id="module-services-emacs-enabling">
-   <title>Enabling the Service</title>
-
-   <para>
-    To install and enable the <command>systemd</command> user service for Emacs
-    daemon, add the following to your <filename>configuration.nix</filename>:
-<programlisting>
-<xref linkend="opt-services.emacs.enable"/> = true;
-<xref linkend="opt-services.emacs.package"/> = import /home/cassou/.emacs.d { pkgs = pkgs; };
-</programlisting>
-   </para>
-
-   <para>
-    The <varname>services.emacs.package</varname> option allows a custom
-    derivation to be used, for example, one created by
-    <function>emacsWithPackages</function>.
-   </para>
-
-   <para>
-    Ensure that the Emacs server is enabled for your user's Emacs
-    configuration, either by customizing the <varname>server-mode</varname>
-    variable, or by adding <literal>(server-start)</literal> to
-    <filename>~/.emacs.d/init.el</filename>.
-   </para>
-
-   <para>
-    To start the daemon, execute the following:
-<screen>
-<prompt>$ </prompt>nixos-rebuild switch  # to activate the new configuration.nix
-<prompt>$ </prompt>systemctl --user daemon-reload        # to force systemd reload
-<prompt>$ </prompt>systemctl --user start emacs.service  # to start the Emacs daemon
-</screen>
-    The server should now be ready to serve Emacs clients.
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-starting-client">
-   <title>Starting the client</title>
-
-   <para>
-    Ensure that the emacs server is enabled, either by customizing the
-    <varname>server-mode</varname> variable, or by adding
-    <literal>(server-start)</literal> to <filename>~/.emacs</filename>.
-   </para>
-
-   <para>
-    To connect to the emacs daemon, run one of the following:
-<programlisting><![CDATA[
-emacsclient FILENAME
-emacsclient --create-frame  # opens a new frame (window)
-emacsclient --create-frame --tty  # opens a new frame on the current terminal
-]]></programlisting>
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-editor-variable">
-   <title>Configuring the <varname>EDITOR</varname> variable</title>
-
-<!--<title><command>emacsclient</command> as the Default Editor</title>-->
-
-   <para>
-    If <xref linkend="opt-services.emacs.defaultEditor"/> is
-    <literal>true</literal>, the <varname>EDITOR</varname> variable will be set
-    to a wrapper script which launches <command>emacsclient</command>.
-   </para>
-
-   <para>
-    Any setting of <varname>EDITOR</varname> in the shell config files will
-    override <varname>services.emacs.defaultEditor</varname>. To make sure
-    <varname>EDITOR</varname> refers to the Emacs wrapper script, remove any
-    existing <varname>EDITOR</varname> assignment from
-    <filename>.profile</filename>, <filename>.bashrc</filename>,
-    <filename>.zshenv</filename> or any other shell config file.
-   </para>
-
-   <para>
-    If you have formed certain bad habits when editing files, these can be
-    corrected with a shell alias to the wrapper script:
-<programlisting>alias vi=$EDITOR</programlisting>
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-per-user">
-   <title>Per-User Enabling of the Service</title>
-
-   <para>
-    In general, <command>systemd</command> user services are globally enabled
-    by symlinks in <filename>/etc/systemd/user</filename>. In the case where
-    Emacs daemon is not wanted for all users, it is possible to install the
-    service but not globally enable it:
-<programlisting>
-<xref linkend="opt-services.emacs.enable"/> = false;
-<xref linkend="opt-services.emacs.install"/> = true;
-</programlisting>
-   </para>
-
-   <para>
-    To enable the <command>systemd</command> user service for just the
-    currently logged in user, run:
-<programlisting>systemctl --user enable emacs</programlisting>
-    This will add the symlink
-    <filename>~/.config/systemd/user/emacs.service</filename>.
-   </para>
-  </section>
- </section>
- <section xml:id="module-services-emacs-configuring">
-  <title>Configuring Emacs</title>
-
-  <para>
-   The Emacs init file should be changed to load the extension packages at
-   startup:
-   <example xml:id="module-services-emacs-package-initialisation">
-    <title>Package initialization in <filename>.emacs</filename></title>
-<programlisting><![CDATA[
-(require 'package)
-
-;; optional. makes unpure packages archives unavailable
-(setq package-archives nil)
-
-(setq package-enable-at-startup nil)
-(package-initialize)
-]]></programlisting>
-   </example>
-  </para>
-
-  <para>
-   After the declarative emacs package configuration has been tested,
-   previously downloaded packages can be cleaned up by removing
-   <filename>~/.emacs.d/elpa</filename> (do make a backup first, in case you
-   forgot a package).
-  </para>
-
-<!--
-      todo: is it worth documenting customizations for
-      server-switch-hook, server-done-hook?
-  -->
-
-  <section xml:id="module-services-emacs-major-mode">
-   <title>A Major Mode for Nix Expressions</title>
-
-   <para>
-    Of interest may be <varname>melpaPackages.nix-mode</varname>, which
-    provides syntax highlighting for the Nix language. This is particularly
-    convenient if you regularly edit Nix files.
-   </para>
-  </section>
-
-  <section xml:id="module-services-emacs-man-pages">
-   <title>Accessing man pages</title>
-
-   <para>
-    You can use <function>woman</function> to get completion of all available
-    man pages. For example, type <literal>M-x woman &lt;RET&gt; nixos-rebuild
-    &lt;RET&gt;.</literal>
-   </para>
-  </section>
-
-  <section xml:id="sec-emacs-docbook-xml">
-   <title>Editing DocBook 5 XML Documents</title>
-
-   <para>
-    Emacs includes
-    <link
-      xlink:href="https://www.gnu.org/software/emacs/manual/html_node/nxml-mode/Introduction.html">nXML</link>,
-    a major-mode for validating and editing XML documents. When editing DocBook
-    5.0 documents, such as <link linkend="book-nixos-manual">this one</link>,
-    nXML needs to be configured with the relevant schema, which is not
-    included.
-   </para>
-
-   <para>
-    To install the DocBook 5.0 schemas, either add
-    <varname>pkgs.docbook5</varname> to
-    <xref linkend="opt-environment.systemPackages"/>
-    (<link
-      linkend="sec-declarative-package-mgmt">NixOS</link>), or run
-    <literal>nix-env -f '&lt;nixpkgs&gt;' -iA docbook5</literal>
-    (<link linkend="sec-ad-hoc-packages">Nix</link>).
-   </para>
-
-   <para>
-    Then customize the variable <varname>rng-schema-locating-files</varname> to
-    include <filename>~/.emacs.d/schemas.xml</filename> and put the following
-    text into that file:
-    <example xml:id="ex-emacs-docbook-xml">
-     <title>nXML Schema Configuration (<filename>~/.emacs.d/schemas.xml</filename>)</title>
-<programlisting language="xml"><![CDATA[
-<?xml version="1.0"?>
-<!--
-  To let emacs find this file, evaluate:
-  (add-to-list 'rng-schema-locating-files "~/.emacs.d/schemas.xml")
--->
-<locatingRules xmlns="http://thaiopensource.com/ns/locating-rules/1.0">
-  <!--
-    Use this variation if pkgs.docbook5 is added to environment.systemPackages
-  -->
-  <namespace ns="http://docbook.org/ns/docbook"
-             uri="/run/current-system/sw/share/xml/docbook-5.0/rng/docbookxi.rnc"/>
-  <!--
-    Use this variation if installing schema with "nix-env -iA pkgs.docbook5".
-  <namespace ns="http://docbook.org/ns/docbook"
-             uri="../.nix-profile/share/xml/docbook-5.0/rng/docbookxi.rnc"/>
-  -->
-</locatingRules>
-]]></programlisting>
-    </example>
-   </para>
-  </section>
- </section>
-</chapter>
diff --git a/nixos/modules/services/games/freeciv.nix b/nixos/modules/services/games/freeciv.nix
index 8b340bb161a..f33ea5c08a2 100644
--- a/nixos/modules/services/games/freeciv.nix
+++ b/nixos/modules/services/games/freeciv.nix
@@ -54,7 +54,7 @@ in
             default = 0;
             description = lib.mdDoc "Set debug log level.";
           };
-          options.exit-on-end = mkEnableOption (lib.mdDoc "exit instead of restarting when a game ends.");
+          options.exit-on-end = mkEnableOption (lib.mdDoc "exit instead of restarting when a game ends");
           options.Guests = mkEnableOption (lib.mdDoc "guests to login if auth is enabled");
           options.Newusers = mkEnableOption (lib.mdDoc "new users to login if auth is enabled");
           options.port = mkOption {
diff --git a/nixos/modules/services/games/minetest-server.nix b/nixos/modules/services/games/minetest-server.nix
index e8c96881673..578364ec542 100644
--- a/nixos/modules/services/games/minetest-server.nix
+++ b/nixos/modules/services/games/minetest-server.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg   = config.services.minetest-server;
-  flag  = val: name: if val != null then "--${name} ${toString val} " else "";
+  flag  = val: name: optionalString (val != null) "--${name} ${toString val} ";
   flags = [
     (flag cfg.gameId "gameid")
     (flag cfg.world "world")
diff --git a/nixos/modules/services/hardware/asusd.nix b/nixos/modules/services/hardware/asusd.nix
index fba9b059bbb..ebbdea26c05 100644
--- a/nixos/modules/services/hardware/asusd.nix
+++ b/nixos/modules/services/hardware/asusd.nix
@@ -2,8 +2,6 @@
 
 let
   cfg = config.services.asusd;
-  json = pkgs.formats.json { };
-  toml = pkgs.formats.toml { };
 in
 {
   options = {
@@ -19,55 +17,55 @@ in
       };
 
       animeConfig = lib.mkOption {
-        type = json.type;
-        default = { };
+        type = lib.types.nullOr lib.types.str;
+        default = null;
         description = lib.mdDoc ''
-          The content of /etc/asusd/anime.conf.
+          The content of /etc/asusd/anime.ron.
           See https://asus-linux.org/asusctl/#anime-control.
         '';
       };
 
       asusdConfig = lib.mkOption {
-        type = json.type;
-        default = { };
+        type = lib.types.nullOr lib.types.str;
+        default = null;
         description = lib.mdDoc ''
-          The content of /etc/asusd/asusd.conf.
+          The content of /etc/asusd/asusd.ron.
           See https://asus-linux.org/asusctl/.
         '';
       };
 
       auraConfig = lib.mkOption {
-        type = json.type;
-        default = { };
+        type = lib.types.nullOr lib.types.str;
+        default = null;
         description = lib.mdDoc ''
-          The content of /etc/asusd/aura.conf.
+          The content of /etc/asusd/aura.ron.
           See https://asus-linux.org/asusctl/#led-keyboard-control.
         '';
       };
 
       profileConfig = lib.mkOption {
         type = lib.types.nullOr lib.types.str;
-        default = "";
+        default = null;
         description = lib.mdDoc ''
-          The content of /etc/asusd/profile.conf.
+          The content of /etc/asusd/profile.ron.
           See https://asus-linux.org/asusctl/#profiles.
         '';
       };
 
-      ledModesConfig = lib.mkOption {
-        type = lib.types.nullOr toml.type;
-        default = null;
-        description = lib.mdDoc ''
-          The content of /etc/asusd/asusd-ledmodes.toml. Leave `null` to use default settings.
-          See https://asus-linux.org/asusctl/#led-keyboard-control.
+      fanCurvesConfig = lib.mkOption {
+      type = lib.types.nullOr lib.types.str;
+      default = null;
+      description = lib.mdDoc ''
+          The content of /etc/asusd/fan_curves.ron.
+          See https://asus-linux.org/asusctl/#fan-curves.
         '';
       };
 
       userLedModesConfig = lib.mkOption {
-        type = lib.types.nullOr toml.type;
+        type = lib.types.nullOr lib.types.str;
         default = null;
         description = lib.mdDoc ''
-          The content of /etc/asusd/asusd-user-ledmodes.toml.
+          The content of /etc/asusd/asusd-user-ledmodes.ron.
           See https://asus-linux.org/asusctl/#led-keyboard-control.
         '';
       };
@@ -79,26 +77,18 @@ in
 
     environment.etc =
       let
-        maybeConfig = name: cfg: lib.mkIf (cfg != { }) {
-          source = json.generate name cfg;
+        maybeConfig = name: cfg: lib.mkIf (cfg != null) {
+          source = pkgs.writeText name cfg;
           mode = "0644";
         };
       in
       {
-        "asusd/anime.conf" = maybeConfig "anime.conf" cfg.animeConfig;
-        "asusd/asusd.conf" = maybeConfig "asusd.conf" cfg.asusdConfig;
-        "asusd/aura.conf" = maybeConfig "aura.conf" cfg.auraConfig;
-        "asusd/profile.conf" = lib.mkIf (cfg.profileConfig != null) {
-          source = pkgs.writeText "profile.conf" cfg.profileConfig;
-          mode = "0644";
-        };
-        "asusd/asusd-ledmodes.toml" = {
-          source =
-            if cfg.ledModesConfig == null
-            then "${pkgs.asusctl}/share/asusd/data/asusd-ledmodes.toml"
-            else toml.generate "asusd-ledmodes.toml" cfg.ledModesConfig;
-          mode = "0644";
-        };
+        "asusd/anime.ron" = maybeConfig "anime.ron" cfg.animeConfig;
+        "asusd/asusd.ron" = maybeConfig "asusd.ron" cfg.asusdConfig;
+        "asusd/aura.ron" = maybeConfig "aura.ron" cfg.auraConfig;
+        "asusd/profile.conf" = maybeConfig "profile.ron" cfg.profileConfig;
+        "asusd/fan_curves.ron" = maybeConfig "fan_curves.ron" cfg.fanCurvesConfig;
+        "asusd/asusd_user_ledmodes.ron" = maybeConfig "asusd_user_ledmodes.ron" cfg.userLedModesConfig;
       };
 
     services.dbus.enable = true;
diff --git a/nixos/modules/services/hardware/auto-cpufreq.nix b/nixos/modules/services/hardware/auto-cpufreq.nix
index 9698e72eb31..fd2e03ef12f 100644
--- a/nixos/modules/services/hardware/auto-cpufreq.nix
+++ b/nixos/modules/services/hardware/auto-cpufreq.nix
@@ -2,10 +2,26 @@
 with lib;
 let
   cfg = config.services.auto-cpufreq;
+  cfgFilename = "auto-cpufreq.conf";
+  cfgFile = format.generate cfgFilename cfg.settings;
+
+  format = pkgs.formats.ini {};
 in {
   options = {
     services.auto-cpufreq = {
       enable = mkEnableOption (lib.mdDoc "auto-cpufreq daemon");
+
+      settings = mkOption {
+        description = lib.mdDoc ''
+          Configuration for `auto-cpufreq`.
+
+          See its [example configuration file] for supported settings.
+          [example configuration file]: https://github.com/AdnanHodzic/auto-cpufreq/blob/master/auto-cpufreq.conf-example
+          '';
+
+        default = {};
+        type = types.submodule { freeformType = format.type; };
+      };
     };
   };
 
@@ -18,6 +34,11 @@ in {
         # Workaround for https://github.com/NixOS/nixpkgs/issues/81138
         wantedBy = [ "multi-user.target" ];
         path = with pkgs; [ bash coreutils ];
+
+        serviceConfig.ExecStart = [
+          ""
+          "${lib.getExe pkgs.auto-cpufreq} --daemon --config ${cfgFile}"
+        ];
       };
     };
   };
diff --git a/nixos/modules/services/hardware/bluetooth.nix b/nixos/modules/services/hardware/bluetooth.nix
index 6453e6968dc..2a58be51bb0 100644
--- a/nixos/modules/services/hardware/bluetooth.nix
+++ b/nixos/modules/services/hardware/bluetooth.nix
@@ -71,6 +71,29 @@ in
         };
         description = lib.mdDoc "Set configuration for system-wide bluetooth (/etc/bluetooth/main.conf).";
       };
+
+      input = mkOption {
+        type = cfgFmt.type;
+        default = { };
+        example = {
+          General = {
+            IdleTimeout = 30;
+            ClassicBondedOnly = true;
+          };
+        };
+        description = lib.mdDoc "Set configuration for the input service (/etc/bluetooth/input.conf).";
+      };
+
+      network = mkOption {
+        type = cfgFmt.type;
+        default = { };
+        example = {
+          General = {
+            DisableSecurity = true;
+          };
+        };
+        description = lib.mdDoc "Set configuration for the network service (/etc/bluetooth/network.conf).";
+      };
     };
   };
 
@@ -80,6 +103,10 @@ in
     environment.systemPackages = [ package ]
       ++ optional cfg.hsphfpd.enable pkgs.hsphfpd;
 
+    environment.etc."bluetooth/input.conf".source =
+      cfgFmt.generate "input.conf" cfg.input;
+    environment.etc."bluetooth/network.conf".source =
+      cfgFmt.generate "network.conf" cfg.network;
     environment.etc."bluetooth/main.conf".source =
       cfgFmt.generate "main.conf" (recursiveUpdate defaults cfg.settings);
     services.udev.packages = [ package ];
diff --git a/nixos/modules/services/hardware/fancontrol.nix b/nixos/modules/services/hardware/fancontrol.nix
index e7eb8ebf92b..993c37b2364 100644
--- a/nixos/modules/services/hardware/fancontrol.nix
+++ b/nixos/modules/services/hardware/fancontrol.nix
@@ -42,6 +42,13 @@ in
         ExecStart = "${pkgs.lm_sensors}/sbin/fancontrol ${configFile}";
       };
     };
+
+    # On some systems, the fancontrol service does not resume properly after sleep because the pwm status of the fans
+    # is not reset properly. Restarting the service fixes this, in accordance with https://github.com/lm-sensors/lm-sensors/issues/172.
+    powerManagement.resumeCommands = ''
+      systemctl restart fancontrol.service
+    '';
+
   };
 
   meta.maintainers = [ maintainers.evils ];
diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix
index 8d7651f97c3..b8c2ac94845 100644
--- a/nixos/modules/services/hardware/fwupd.nix
+++ b/nixos/modules/services/hardware/fwupd.nix
@@ -18,11 +18,11 @@ let
         fwupd = cfg.daemonSettings;
       };
     };
+
     "fwupd/uefi_capsule.conf" = {
-      source = pkgs.writeText "uefi_capsule.conf" ''
-        [uefi_capsule]
-        OverrideESPMountPoint=${config.boot.loader.efi.efiSysMountPoint}
-      '';
+      source = format.generate "uefi_capsule.conf" {
+        uefi_capsule = cfg.uefiCapsuleSettings;
+      };
     };
   };
 
@@ -127,6 +127,16 @@ in {
                 List of plugins to be disabled.
               '';
             };
+
+            EspLocation = mkOption {
+              type = types.path;
+              default = config.boot.loader.efi.efiSysMountPoint;
+              defaultText = lib.literalExpression "config.boot.loader.efi.efiSysMountPoint";
+              description = lib.mdDoc ''
+                The EFI system partition (ESP) path used if UDisks is not available
+                or if this partition is not mounted at /boot/efi, /boot, or /efi
+              '';
+            };
           };
         };
         default = {};
@@ -134,6 +144,16 @@ in {
           Configurations for the fwupd daemon.
         '';
       };
+
+      uefiCapsuleSettings = mkOption {
+        type = types.submodule {
+          freeformType = format.type.nestedTypes.elemType;
+        };
+        default = {};
+        description = lib.mdDoc ''
+          UEFI capsule configurations for the fwupd daemon.
+        '';
+      };
     };
   };
 
@@ -147,7 +167,10 @@ in {
   ###### implementation
   config = mkIf cfg.enable {
     # Disable test related plug-ins implicitly so that users do not have to care about them.
-    services.fwupd.daemonSettings.DisabledPlugins = cfg.package.defaultDisabledPlugins;
+    services.fwupd.daemonSettings = {
+      DisabledPlugins = cfg.package.defaultDisabledPlugins;
+      EspLocation = config.boot.loader.efi.efiSysMountPoint;
+    };
 
     environment.systemPackages = [ cfg.package ];
 
@@ -158,6 +181,9 @@ in {
 
     services.udev.packages = [ cfg.package ];
 
+    # required to update the firmware of disks
+    services.udisks2.enable = true;
+
     systemd.packages = [ cfg.package ];
 
     security.polkit.enable = true;
diff --git a/nixos/modules/services/hardware/kanata.nix b/nixos/modules/services/hardware/kanata.nix
index 84265eb8f94..7d544050130 100644
--- a/nixos/modules/services/hardware/kanata.nix
+++ b/nixos/modules/services/hardware/kanata.nix
@@ -8,19 +8,9 @@ let
   keyboard = {
     options = {
       devices = mkOption {
-        type = types.addCheck (types.listOf types.str)
-          (devices: (length devices) > 0);
+        type = types.listOf types.str;
         example = [ "/dev/input/by-id/usb-0000_0000-event-kbd" ];
-        # TODO replace note with tip, which has not been implemented yet in
-        # nixos/lib/make-options-doc/mergeJSON.py
-        description = mdDoc ''
-          Paths to keyboard devices.
-
-          ::: {.note}
-          To avoid unnecessary triggers of the service unit, unplug devices in
-          the order of the list.
-          :::
-        '';
+        description = mdDoc "Paths to keyboard devices.";
       };
       config = mkOption {
         type = types.lines;
@@ -44,8 +34,10 @@ let
             cap (tap-hold 100 100 caps lctl))
         '';
         description = mdDoc ''
-          Configuration other than `defcfg`. See [example config
-          files](https://github.com/jtroo/kanata) for more information.
+          Configuration other than `defcfg`.
+
+          See [example config files](https://github.com/jtroo/kanata)
+          for more information.
         '';
       };
       extraDefCfg = mkOption {
@@ -53,8 +45,12 @@ let
         default = "";
         example = "danger-enable-cmd yes";
         description = mdDoc ''
-          Configuration of `defcfg` other than `linux-dev`. See [example
-          config files](https://github.com/jtroo/kanata) for more information.
+          Configuration of `defcfg` other than `linux-dev` (generated
+          from the devices option) and
+          `linux-continue-if-no-devs-found` (hardcoded to be yes).
+
+          See [example config files](https://github.com/jtroo/kanata)
+          for more information.
         '';
       };
       extraArgs = mkOption {
@@ -67,8 +63,7 @@ let
         default = null;
         example = 6666;
         description = mdDoc ''
-          Port to run the notification server on. `null` will not run the
-          server.
+          Port to run the TCP server on. `null` will not run the server.
         '';
       };
     };
@@ -76,28 +71,24 @@ let
 
   mkName = name: "kanata-${name}";
 
-  mkDevices = devices: concatStringsSep ":" devices;
+  mkDevices = devices:
+    optionalString ((length devices) > 0) "linux-dev ${concatStringsSep ":" devices}";
 
   mkConfig = name: keyboard: pkgs.writeText "${mkName name}-config.kdb" ''
     (defcfg
       ${keyboard.extraDefCfg}
-      linux-dev ${mkDevices keyboard.devices})
+      ${mkDevices keyboard.devices}
+      linux-continue-if-no-devs-found yes)
 
     ${keyboard.config}
   '';
 
   mkService = name: keyboard: nameValuePair (mkName name) {
-    description = "kanata for ${mkDevices keyboard.devices}";
-
-    # Because path units are used to activate service units, which
-    # will start the old stopped services during "nixos-rebuild
-    # switch", stopIfChanged here is a workaround to make sure new
-    # services are running after "nixos-rebuild switch".
-    stopIfChanged = false;
-
+    wantedBy = [ "multi-user.target" ];
     serviceConfig = {
+      Type = "notify";
       ExecStart = ''
-        ${cfg.package}/bin/kanata \
+        ${getExe cfg.package} \
           --cfg ${mkConfig name keyboard} \
           --symlink-path ''${RUNTIME_DIRECTORY}/${name} \
           ${optionalString (keyboard.port != null) "--port ${toString keyboard.port}"} \
@@ -133,8 +124,7 @@ let
       ProtectKernelModules = true;
       ProtectKernelTunables = true;
       ProtectProc = "invisible";
-      RestrictAddressFamilies =
-        if (keyboard.port == null) then "none" else [ "AF_INET" ];
+      RestrictAddressFamilies = [ "AF_UNIX" ] ++ optional (keyboard.port != null) "AF_INET";
       RestrictNamespaces = true;
       RestrictRealtime = true;
       SystemCallArchitectures = [ "native" ];
@@ -146,37 +136,10 @@ let
       UMask = "0077";
     };
   };
-
-  mkPathName = i: name: "${mkName name}-${toString i}";
-
-  mkPath = name: n: i: device:
-    nameValuePair (mkPathName i name) {
-      description =
-        "${toString (i+1)}/${toString n} kanata trigger for ${name}, watching ${device}";
-      wantedBy = optional (i == 0) "multi-user.target";
-      pathConfig = {
-        PathExists = device;
-        # (ab)use systemd.path to construct a trigger chain so that the
-        # service unit is only started when all paths exist
-        # however, manual of systemd.path says Unit's suffix is not ".path"
-        Unit =
-          if (i + 1) == n
-          then "${mkName name}.service"
-          else "${mkPathName (i + 1) name}.path";
-      };
-      unitConfig.StopPropagatedFrom = optional (i > 0) "${mkName name}.service";
-    };
-
-  mkPaths = name: keyboard:
-    let
-      n = length keyboard.devices;
-    in
-    imap0 (mkPath name n) keyboard.devices
-  ;
 in
 {
   options.services.kanata = {
-    enable = mkEnableOption (lib.mdDoc "kanata");
+    enable = mkEnableOption (mdDoc "kanata");
     package = mkOption {
       type = types.package;
       default = pkgs.kanata;
@@ -201,14 +164,7 @@ in
   config = mkIf cfg.enable {
     hardware.uinput.enable = true;
 
-    systemd = {
-      paths = trivial.pipe cfg.keyboards [
-        (mapAttrsToList mkPaths)
-        concatLists
-        listToAttrs
-      ];
-      services = mapAttrs' mkService cfg.keyboards;
-    };
+    systemd.services = mapAttrs' mkService cfg.keyboards;
   };
 
   meta.maintainers = with maintainers; [ linj ];
diff --git a/nixos/modules/services/hardware/keyd.nix b/nixos/modules/services/hardware/keyd.nix
new file mode 100644
index 00000000000..64c769405fa
--- /dev/null
+++ b/nixos/modules/services/hardware/keyd.nix
@@ -0,0 +1,112 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.keyd;
+  settingsFormat = pkgs.formats.ini { };
+in
+{
+  options = {
+    services.keyd = {
+      enable = mkEnableOption (lib.mdDoc "keyd, a key remapping daemon");
+
+      ids = mkOption {
+        type = types.listOf types.string;
+        default = [ "*" ];
+        example = [ "*" "-0123:0456" ];
+        description = lib.mdDoc ''
+          Device identifiers, as shown by {manpage}`keyd(1)`.
+        '';
+      };
+
+      settings = mkOption {
+        type = settingsFormat.type;
+        default = { };
+        example = {
+          main = {
+            capslock = "overload(control, esc)";
+            rightalt = "layer(rightalt)";
+          };
+
+          rightalt = {
+            j = "down";
+            k = "up";
+            h = "left";
+            l = "right";
+          };
+        };
+        description = lib.mdDoc ''
+          Configuration, except `ids` section, that is written to {file}`/etc/keyd/default.conf`.
+          See <https://github.com/rvaiya/keyd> how to configure.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."keyd/default.conf".source = pkgs.runCommand "default.conf"
+      {
+        ids = ''
+          [ids]
+          ${concatStringsSep "\n" cfg.ids}
+        '';
+        passAsFile = [ "ids" ];
+      } ''
+      cat $idsPath <(echo) ${settingsFormat.generate "keyd-main.conf" cfg.settings} >$out
+    '';
+
+    hardware.uinput.enable = lib.mkDefault true;
+
+    systemd.services.keyd = {
+      description = "Keyd remapping daemon";
+      documentation = [ "man:keyd(1)" ];
+
+      wantedBy = [ "multi-user.target" ];
+
+      restartTriggers = [
+        config.environment.etc."keyd/default.conf".source
+      ];
+
+      # this is configurable in 2.4.2, later versions seem to remove this option.
+      # post-2.4.2 may need to set makeFlags in the derivation:
+      #
+      #     makeFlags = [ "SOCKET_PATH/run/keyd/keyd.socket" ];
+      environment.KEYD_SOCKET = "/run/keyd/keyd.sock";
+
+      serviceConfig = {
+        ExecStart = "${pkgs.keyd}/bin/keyd";
+        Restart = "always";
+
+        DynamicUser = true;
+        SupplementaryGroups = [
+          config.users.groups.input.name
+          config.users.groups.uinput.name
+        ];
+
+        RuntimeDirectory = "keyd";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        DeviceAllow = [
+          "char-input rw"
+          "/dev/uinput rw"
+        ];
+        ProtectClock = true;
+        PrivateNetwork = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        PrivateUsers = true;
+        PrivateMounts = true;
+        RestrictNamespaces = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        LockPersonality = true;
+        ProtectProc = "noaccess";
+        UMask = "0077";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/hardware/sane.nix b/nixos/modules/services/hardware/sane.nix
index 60354c7644f..2cac2e8e8bb 100644
--- a/nixos/modules/services/hardware/sane.nix
+++ b/nixos/modules/services/hardware/sane.nix
@@ -28,7 +28,7 @@ let
   };
 
   env = {
-    SANE_CONFIG_DIR = "/etc/sane.d";
+    SANE_CONFIG_DIR = "/etc/sane-config";
     LD_LIBRARY_PATH = [ "/etc/sane-libs" ];
   };
 
@@ -167,7 +167,7 @@ in
 
       environment.systemPackages = backends;
       environment.sessionVariables = env;
-      environment.etc."sane.d".source = config.hardware.sane.configDir;
+      environment.etc."sane-config".source = config.hardware.sane.configDir;
       environment.etc."sane-libs".source = "${saneConfig}/lib/sane";
       services.udev.packages = backends;
 
diff --git a/nixos/modules/services/hardware/supergfxd.nix b/nixos/modules/services/hardware/supergfxd.nix
index cb604db91dc..5ea05ac2771 100644
--- a/nixos/modules/services/hardware/supergfxd.nix
+++ b/nixos/modules/services/hardware/supergfxd.nix
@@ -23,12 +23,16 @@ in
   config = lib.mkIf cfg.enable {
     environment.systemPackages = [ pkgs.supergfxctl ];
 
-    environment.etc."supergfxd.conf" = lib.mkIf (cfg.settings != null) { source = json.generate "supergfxd.conf" cfg.settings; };
+    environment.etc."supergfxd.conf" = lib.mkIf (cfg.settings != null) {
+      source = json.generate "supergfxd.conf" cfg.settings;
+      mode = "0644";
+    };
 
     services.dbus.enable = true;
 
     systemd.packages = [ pkgs.supergfxctl ];
     systemd.services.supergfxd.wantedBy = [ "multi-user.target" ];
+    systemd.services.supergfxd.path = [ pkgs.kmod ];
 
     services.dbus.packages = [ pkgs.supergfxctl ];
     services.udev.packages = [ pkgs.supergfxctl ];
diff --git a/nixos/modules/services/hardware/throttled.nix b/nixos/modules/services/hardware/throttled.nix
index 99735ff6519..afca24d976e 100644
--- a/nixos/modules/services/hardware/throttled.nix
+++ b/nixos/modules/services/hardware/throttled.nix
@@ -20,12 +20,12 @@ in {
   config = mkIf cfg.enable {
     systemd.packages = [ pkgs.throttled ];
     # The upstream package has this in Install, but that's not enough, see the NixOS manual
-    systemd.services.lenovo_fix.wantedBy = [ "multi-user.target" ];
+    systemd.services.throttled.wantedBy = [ "multi-user.target" ];
 
-    environment.etc."lenovo_fix.conf".source =
+    environment.etc."throttled.conf".source =
       if cfg.extraConfig != ""
-      then pkgs.writeText "lenovo_fix.conf" cfg.extraConfig
-      else "${pkgs.throttled}/etc/lenovo_fix.conf";
+      then pkgs.writeText "throttled.conf" cfg.extraConfig
+      else "${pkgs.throttled}/etc/throttled.conf";
 
     # Kernel 5.9 spams warnings whenever userspace writes to CPU MSRs.
     # See https://github.com/erpalma/throttled/issues/215
diff --git a/nixos/modules/services/hardware/trezord.md b/nixos/modules/services/hardware/trezord.md
new file mode 100644
index 00000000000..58c244a44bc
--- /dev/null
+++ b/nixos/modules/services/hardware/trezord.md
@@ -0,0 +1,17 @@
+# Trezor {#trezor}
+
+Trezor is an open-source cryptocurrency hardware wallet and security token
+allowing secure storage of private keys.
+
+It offers advanced features such U2F two-factor authorization, SSH login
+through
+[Trezor SSH agent](https://wiki.trezor.io/Apps:SSH_agent),
+[GPG](https://wiki.trezor.io/GPG) and a
+[password manager](https://wiki.trezor.io/Trezor_Password_Manager).
+For more information, guides and documentation, see <https://wiki.trezor.io>.
+
+To enable Trezor support, add the following to your {file}`configuration.nix`:
+
+    services.trezord.enable = true;
+
+This will add all necessary udev rules and start Trezor Bridge.
diff --git a/nixos/modules/services/hardware/trezord.nix b/nixos/modules/services/hardware/trezord.nix
index 70c1fd09860..b2217fc9712 100644
--- a/nixos/modules/services/hardware/trezord.nix
+++ b/nixos/modules/services/hardware/trezord.nix
@@ -8,7 +8,7 @@ in {
   ### docs
 
   meta = {
-    doc = ./trezord.xml;
+    doc = ./trezord.md;
   };
 
   ### interface
diff --git a/nixos/modules/services/hardware/trezord.xml b/nixos/modules/services/hardware/trezord.xml
deleted file mode 100644
index 972d409d9d0..00000000000
--- a/nixos/modules/services/hardware/trezord.xml
+++ /dev/null
@@ -1,26 +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="trezor">
- <title>Trezor</title>
- <para>
-  Trezor is an open-source cryptocurrency hardware wallet and security token
-  allowing secure storage of private keys.
- </para>
- <para>
-  It offers advanced features such U2F two-factor authorization, SSH login
-  through
-  <link xlink:href="https://wiki.trezor.io/Apps:SSH_agent">Trezor SSH agent</link>,
-  <link xlink:href="https://wiki.trezor.io/GPG">GPG</link> and a
-  <link xlink:href="https://wiki.trezor.io/Trezor_Password_Manager">password manager</link>.
-  For more information, guides and documentation, see <link xlink:href="https://wiki.trezor.io"/>.
- </para>
- <para>
-  To enable Trezor support, add the following to your <filename>configuration.nix</filename>:
-<programlisting>
-<xref linkend="opt-services.trezord.enable"/> = true;
-</programlisting>
-  This will add all necessary udev rules and start Trezor Bridge.
- </para>
-</chapter>
diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix
index d9526133241..94406b60b29 100644
--- a/nixos/modules/services/hardware/udev.nix
+++ b/nixos/modules/services/hardware/udev.nix
@@ -16,16 +16,6 @@ let
   '';
 
 
-  # networkd link files are used early by udev to set up interfaces early.
-  # This must be done in stage 1 to avoid race conditions between udev and
-  # network daemons.
-  # 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;
@@ -170,7 +160,7 @@ let
 
       echo "Generating hwdb database..."
       # hwdb --update doesn't return error code even on errors!
-      res="$(${pkgs.buildPackages.udev}/bin/udevadm hwdb --update --root=$(pwd) 2>&1)"
+      res="$(${pkgs.buildPackages.systemd}/bin/systemd-hwdb --root=$(pwd) update 2>&1)"
       echo "$res"
       [ -z "$(echo "$res" | egrep '^Error')" ]
       mv etc/udev/hwdb.bin $out
@@ -398,7 +388,6 @@ in
         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 = [
diff --git a/nixos/modules/services/hardware/udisks2.nix b/nixos/modules/services/hardware/udisks2.nix
index 7368845dafd..c53dbf47774 100644
--- a/nixos/modules/services/hardware/udisks2.nix
+++ b/nixos/modules/services/hardware/udisks2.nix
@@ -1,10 +1,9 @@
 # Udisks daemon.
-
 { config, lib, pkgs, ... }:
-
 with lib;
 
 let
+  cfg = config.services.udisks2;
   settingsFormat = pkgs.formats.ini {
     listToValue = concatMapStringsSep "," (generators.mkValueStringDefault {});
   };
@@ -19,7 +18,17 @@ in
 
     services.udisks2 = {
 
-      enable = mkEnableOption (lib.mdDoc "udisks2, a DBus service that allows applications to query and manipulate storage devices.");
+      enable = mkEnableOption (mdDoc "udisks2, a DBus service that allows applications to query and manipulate storage devices");
+
+      mountOnMedia = mkOption {
+        type = types.bool;
+        default = false;
+        description = mdDoc ''
+          When enabled, instructs udisks2 to mount removable drives under `/media/` directory, instead of the
+          default, ACL-controlled `/run/media/$USER/`. Since `/media/` is not mounted as tmpfs by default, it
+          requires cleanup to get rid of stale mountpoints; enabling this option will take care of this at boot.
+        '';
+      };
 
       settings = mkOption rec {
         type = types.attrsOf settingsFormat.type;
@@ -44,7 +53,7 @@ in
           };
         };
         '';
-        description = lib.mdDoc ''
+        description = mdDoc ''
           Options passed to udisksd.
           See [here](http://manpages.ubuntu.com/manpages/latest/en/man5/udisks2.conf.5.html) and
           drive configuration in [here](http://manpages.ubuntu.com/manpages/latest/en/man8/udisks.8.html) for supported options.
@@ -73,10 +82,15 @@ in
 
     services.dbus.packages = [ pkgs.udisks2 ];
 
-    systemd.tmpfiles.rules = [ "d /var/lib/udisks2 0755 root root -" ];
+    systemd.tmpfiles.rules = [ "d /var/lib/udisks2 0755 root root -" ]
+      ++ optional cfg.mountOnMedia "D! /media 0755 root root -";
 
     services.udev.packages = [ pkgs.udisks2 ];
 
+    services.udev.extraRules = optionalString cfg.mountOnMedia ''
+      ENV{ID_FS_USAGE}=="filesystem", ENV{UDISKS_FILESYSTEM_SHARED}="1"
+    '';
+
     systemd.packages = [ pkgs.udisks2 ];
   };
 
diff --git a/nixos/modules/services/hardware/undervolt.nix b/nixos/modules/services/hardware/undervolt.nix
index c49d944cdc1..94477747540 100644
--- a/nixos/modules/services/hardware/undervolt.nix
+++ b/nixos/modules/services/hardware/undervolt.nix
@@ -5,8 +5,8 @@ let
   cfg = config.services.undervolt;
 
   mkPLimit = limit: window:
-    if (isNull limit && isNull window) then null
-    else assert asserts.assertMsg (!isNull limit && !isNull window) "Both power limit and window must be set";
+    if (limit == null && window == null) then null
+    else assert asserts.assertMsg (limit != null && window != null) "Both power limit and window must be set";
       "${toString limit} ${toString window}";
   cliArgs = lib.cli.toGNUCommandLine {} {
     inherit (cfg)
diff --git a/nixos/modules/services/home-automation/esphome.nix b/nixos/modules/services/home-automation/esphome.nix
new file mode 100644
index 00000000000..d7dbb6f0b90
--- /dev/null
+++ b/nixos/modules/services/home-automation/esphome.nix
@@ -0,0 +1,136 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib)
+    literalExpression
+    maintainers
+    mkEnableOption
+    mkIf
+    mkOption
+    mdDoc
+    types
+    ;
+
+  cfg = config.services.esphome;
+
+  stateDir = "/var/lib/esphome";
+
+  esphomeParams =
+    if cfg.enableUnixSocket
+    then "--socket /run/esphome/esphome.sock"
+    else "--address ${cfg.address} --port ${toString cfg.port}";
+in
+{
+  meta.maintainers = with maintainers; [ oddlama ];
+
+  options.services.esphome = {
+    enable = mkEnableOption (mdDoc "esphome");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.esphome;
+      defaultText = literalExpression "pkgs.esphome";
+      description = mdDoc "The package to use for the esphome command.";
+    };
+
+    enableUnixSocket = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc "Listen on a unix socket `/run/esphome/esphome.sock` instead of the TCP port.";
+    };
+
+    address = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = mdDoc "esphome address";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 6052;
+      description = mdDoc "esphome port";
+    };
+
+    openFirewall = mkOption {
+      default = false;
+      type = types.bool;
+      description = mdDoc "Whether to open the firewall for the specified port.";
+    };
+
+    allowedDevices = mkOption {
+      default = ["char-ttyS" "char-ttyUSB"];
+      example = ["/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0"];
+      description = lib.mdDoc ''
+        A list of device nodes to which {command}`esphome` has access to.
+        Refer to DeviceAllow in systemd.resource-control(5) for more information.
+        Beware that if a device is referred to by an absolute path instead of a device category,
+        it will only allow devices that already are plugged in when the service is started.
+      '';
+      type = types.listOf types.str;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall && !cfg.enableUnixSocket) [cfg.port];
+
+    systemd.services.esphome = {
+      description = "ESPHome dashboard";
+      after = ["network.target"];
+      wantedBy = ["multi-user.target"];
+      path = [cfg.package];
+
+      # platformio fails to determine the home directory when using DynamicUser
+      environment.PLATFORMIO_CORE_DIR = "${stateDir}/.platformio";
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/esphome dashboard ${esphomeParams} ${stateDir}";
+        DynamicUser = true;
+        User = "esphome";
+        Group = "esphome";
+        WorkingDirectory = stateDir;
+        StateDirectory = "esphome";
+        StateDirectoryMode = "0750";
+        Restart = "on-failure";
+        RuntimeDirectory = mkIf cfg.enableUnixSocket "esphome";
+        RuntimeDirectoryMode = "0750";
+
+        # Hardening
+        CapabilityBoundingSet = "";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        DevicePolicy = "closed";
+        DeviceAllow = map (d: "${d} rw") cfg.allowedDevices;
+        SupplementaryGroups = ["dialout"];
+        #NoNewPrivileges = true; # Implied by DynamicUser
+        PrivateUsers = true;
+        #PrivateTmp = true; # Implied by DynamicUser
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        ProtectSystem = "strict";
+        #RemoveIPC = true; # Implied by DynamicUser
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+          "AF_NETLINK"
+          "AF_UNIX"
+        ];
+        RestrictNamespaces = false; # Required by platformio for chroot
+        RestrictRealtime = true;
+        #RestrictSUIDSGID = true; # Implied by DynamicUser
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [
+          "@system-service"
+          "@mount" # Required by platformio for chroot
+        ];
+        UMask = "0077";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
index fa06e5391bb..abe0b93e412 100644
--- a/nixos/modules/services/home-automation/home-assistant.nix
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -35,7 +35,10 @@ let
   #   ...
   # } ];
   usedPlatforms = config:
-    if isAttrs config then
+    # don't recurse into derivations possibly creating an infinite recursion
+    if isDerivation config then
+      [ ]
+    else if isAttrs config then
       optional (config ? platform) config.platform
       ++ concatMap usedPlatforms (attrValues config)
     else if isList config then
@@ -362,7 +365,7 @@ in {
   config = mkIf cfg.enable {
     assertions = [
       {
-        assertion = cfg.openFirewall -> !isNull cfg.config;
+        assertion = cfg.openFirewall -> cfg.config != null;
         message = "openFirewall can only be used with a declarative config";
       }
     ];
@@ -409,6 +412,7 @@ in {
         (optionalString (cfg.config != null) copyConfig) +
         (optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig)
       ;
+      environment.PYTHONPATH = package.pythonPath;
       serviceConfig = let
         # List of capabilities to equip home-assistant with, depending on configured components
         capabilities = lib.unique ([
@@ -438,11 +442,13 @@ in {
           "aranet"
           "bluemaestro"
           "bluetooth"
+          "bluetooth_adapters"
           "bluetooth_le_tracker"
           "bluetooth_tracker"
           "bthome"
           "default_config"
           "eq3btsmart"
+          "eufylife_ble"
           "esphome"
           "fjaraskupan"
           "govee_ble"
@@ -452,8 +458,11 @@ in {
           "led_ble"
           "melnor"
           "moat"
+          "mopeka"
           "oralb"
           "qingping"
+          "rapt_ble"
+          "ruuvi_gateway"
           "ruuvitag_ble"
           "sensirion_ble"
           "sensorpro"
@@ -500,6 +509,7 @@ in {
           "mysensors"
           "nad"
           "numato"
+          "otbr"
           "rflink"
           "rfxtrx"
           "scsgate"
diff --git a/nixos/modules/services/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix
index 1799e9282b3..342ac5ec6e0 100644
--- a/nixos/modules/services/logging/logrotate.nix
+++ b/nixos/modules/services/logging/logrotate.nix
@@ -83,9 +83,8 @@ let
   };
 
   mailOption =
-    if foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings)
-    then "--mail=${pkgs.mailutils}/bin/mail"
-    else "";
+    optionalString (foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings))
+    "--mail=${pkgs.mailutils}/bin/mail";
 in
 {
   imports = [
@@ -187,7 +186,7 @@ in
           A configuration file automatically generated by NixOS.
         '';
         description = lib.mdDoc ''
-          Override the configuration file used by MySQL. By default,
+          Override the configuration file used by logrotate. By default,
           NixOS generates one automatically from [](#opt-services.logrotate.settings).
         '';
         example = literalExpression ''
diff --git a/nixos/modules/services/logging/syslogd.nix b/nixos/modules/services/logging/syslogd.nix
index 43969402588..553973e255f 100644
--- a/nixos/modules/services/logging/syslogd.nix
+++ b/nixos/modules/services/logging/syslogd.nix
@@ -7,7 +7,7 @@ let
   cfg = config.services.syslogd;
 
   syslogConf = pkgs.writeText "syslog.conf" ''
-    ${if (cfg.tty != "") then "kern.warning;*.err;authpriv.none /dev/${cfg.tty}" else ""}
+    ${optionalString (cfg.tty != "") "kern.warning;*.err;authpriv.none /dev/${cfg.tty}"}
     ${cfg.defaultConfig}
     ${cfg.extraConfig}
   '';
diff --git a/nixos/modules/services/logging/ulogd.nix b/nixos/modules/services/logging/ulogd.nix
new file mode 100644
index 00000000000..065032b531c
--- /dev/null
+++ b/nixos/modules/services/logging/ulogd.nix
@@ -0,0 +1,48 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.ulogd;
+  settingsFormat = pkgs.formats.ini { };
+  settingsFile = settingsFormat.generate "ulogd.conf" cfg.settings;
+in {
+  options = {
+    services.ulogd = {
+      enable = mkEnableOption (lib.mdDoc "ulogd");
+
+      settings = mkOption {
+        example = {
+          global.stack = "stack=log1:NFLOG,base1:BASE,pcap1:PCAP";
+          log1.group = 2;
+          pcap1 = {
+            file = "/var/log/ulogd.pcap";
+            sync = 1;
+          };
+        };
+        type = settingsFormat.type;
+        default = { };
+        description = lib.mdDoc "Configuration for ulogd. See {file}`/share/doc/ulogd/` in `pkgs.ulogd.doc`.";
+      };
+
+      logLevel = mkOption {
+        type = types.enum [ 1 3 5 7 8 ];
+        default = 5;
+        description = lib.mdDoc "Log level (1 = debug, 3 = info, 5 = notice, 7 = error, 8 = fatal)";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.ulogd = {
+      description = "Ulogd Daemon";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-pre.target" ];
+      before = [ "network-pre.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.ulogd}/bin/ulogd -c ${settingsFile} --verbose --loglevel ${toString cfg.logLevel}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/logging/vector.nix b/nixos/modules/services/logging/vector.nix
index 1803ea85e49..a923373d186 100644
--- a/nixos/modules/services/logging/vector.nix
+++ b/nixos/modules/services/logging/vector.nix
@@ -26,13 +26,9 @@ in
   };
 
   config = mkIf cfg.enable {
+    # for cli usage
+    environment.systemPackages = [ pkgs.vector ];
 
-    users.groups.vector = { };
-    users.users.vector = {
-      description = "Vector service user";
-      group = "vector";
-      isSystemUser = true;
-    };
     systemd.services.vector = {
       description = "Vector event and log aggregator";
       wantedBy = [ "multi-user.target" ];
@@ -52,8 +48,7 @@ in
         in
         {
           ExecStart = "${pkgs.vector}/bin/vector --config ${validateConfig conf}";
-          User = "vector";
-          Group = "vector";
+          DynamicUser = true;
           Restart = "no";
           StateDirectory = "vector";
           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix
index f6a167572f7..21bafd859c3 100644
--- a/nixos/modules/services/mail/dovecot.nix
+++ b/nixos/modules/services/mail/dovecot.nix
@@ -171,11 +171,11 @@ in
   options.services.dovecot2 = {
     enable = mkEnableOption (lib.mdDoc "the dovecot 2.x POP3/IMAP server");
 
-    enablePop3 = mkEnableOption (lib.mdDoc "starting the POP3 listener (when Dovecot is enabled).");
+    enablePop3 = mkEnableOption (lib.mdDoc "starting the POP3 listener (when Dovecot is enabled)");
 
-    enableImap = mkEnableOption (lib.mdDoc "starting the IMAP listener (when Dovecot is enabled).") // { default = true; };
+    enableImap = mkEnableOption (lib.mdDoc "starting the IMAP listener (when Dovecot is enabled)") // { default = true; };
 
-    enableLmtp = mkEnableOption (lib.mdDoc "starting the LMTP listener (when Dovecot is enabled).");
+    enableLmtp = mkEnableOption (lib.mdDoc "starting the LMTP listener (when Dovecot is enabled)");
 
     protocols = mkOption {
       type = types.listOf types.str;
@@ -300,9 +300,9 @@ in
       description = lib.mdDoc "Path to the server's private key.";
     };
 
-    enablePAM = mkEnableOption (lib.mdDoc "creating a own Dovecot PAM service and configure PAM user logins.") // { default = true; };
+    enablePAM = mkEnableOption (lib.mdDoc "creating a own Dovecot PAM service and configure PAM user logins") // { default = true; };
 
-    enableDHE = mkEnableOption (lib.mdDoc "enable ssl_dh and generation of primes for the key exchange.") // { default = true; };
+    enableDHE = mkEnableOption (lib.mdDoc "enable ssl_dh and generation of primes for the key exchange") // { default = true; };
 
     sieveScripts = mkOption {
       type = types.attrsOf types.path;
@@ -310,7 +310,7 @@ in
       description = lib.mdDoc "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
     };
 
-    showPAMFailure = mkEnableOption (lib.mdDoc "showing the PAM failure message on authentication error (useful for OTPW).");
+    showPAMFailure = mkEnableOption (lib.mdDoc "showing the PAM failure message on authentication error (useful for OTPW)");
 
     mailboxes = mkOption {
       type = with types; coercedTo
@@ -326,7 +326,7 @@ in
       description = lib.mdDoc "Configure mailboxes and auto create or subscribe them.";
     };
 
-    enableQuota = mkEnableOption (lib.mdDoc "the dovecot quota service.");
+    enableQuota = mkEnableOption (lib.mdDoc "the dovecot quota service");
 
     quotaPort = mkOption {
       type = types.str;
diff --git a/nixos/modules/services/mail/exim.nix b/nixos/modules/services/mail/exim.nix
index cd0da4fc509..a9504acee35 100644
--- a/nixos/modules/services/mail/exim.nix
+++ b/nixos/modules/services/mail/exim.nix
@@ -116,8 +116,9 @@ in
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ config.environment.etc."exim.conf".source ];
       serviceConfig = {
-        ExecStart   = "${cfg.package}/bin/exim -bdf -q${cfg.queueRunnerInterval}";
-        ExecReload  = "${coreutils}/bin/kill -HUP $MAINPID";
+        ExecStart   = "+${cfg.package}/bin/exim -bdf -q${cfg.queueRunnerInterval}";
+        ExecReload  = "+${coreutils}/bin/kill -HUP $MAINPID";
+        User        = cfg.user;
       };
       preStart = ''
         if ! test -d ${cfg.spoolDir}; then
diff --git a/nixos/modules/services/mail/goeland.nix b/nixos/modules/services/mail/goeland.nix
new file mode 100644
index 00000000000..13092a65ed9
--- /dev/null
+++ b/nixos/modules/services/mail/goeland.nix
@@ -0,0 +1,74 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.goeland;
+  tomlFormat = pkgs.formats.toml { };
+in
+{
+  options.services.goeland = {
+    enable = mkEnableOption (mdDoc "goeland");
+
+    settings = mkOption {
+      description = mdDoc ''
+        Configuration of goeland.
+        See the [example config file](https://github.com/slurdge/goeland/blob/master/cmd/asset/config.default.toml) for the available options.
+      '';
+      default = { };
+      type = tomlFormat.type;
+    };
+    schedule = mkOption {
+      type = types.str;
+      default = "12h";
+      example = "Mon, 00:00:00";
+      description = mdDoc "How often to run goeland, in systemd time format.";
+    };
+    stateDir = mkOption {
+      type = types.path;
+      default = "/var/lib/goeland";
+      description = mdDoc ''
+        The data directory for goeland where the database will reside if using the unseen filter.
+        If left as the default value this directory will automatically be created before the goeland
+        server starts, otherwise you are responsible for ensuring the directory exists with
+        appropriate ownership and permissions.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.goeland.settings.database = "${cfg.stateDir}/goeland.db";
+
+    systemd.services.goeland = {
+      serviceConfig = let confFile = tomlFormat.generate "config.toml" cfg.settings; in mkMerge [
+        {
+          ExecStart = "${pkgs.goeland}/bin/goeland run -c ${confFile}";
+          User = "goeland";
+          Group = "goeland";
+        }
+        (mkIf (cfg.stateDir == "/var/lib/goeland") {
+          StateDirectory = "goeland";
+          StateDirectoryMode = "0750";
+        })
+      ];
+      startAt = cfg.schedule;
+    };
+
+    users.users.goeland = {
+      description = "goeland user";
+      group = "goeland";
+      isSystemUser = true;
+    };
+    users.groups.goeland = { };
+
+    warnings = optionals (hasAttr "password" cfg.settings.email) [
+      ''
+        It is not recommended to set the "services.goeland.settings.email.password"
+        option as it will be in cleartext in the Nix store.
+        Please use "services.goeland.settings.email.password_file" instead.
+      ''
+    ];
+  };
+
+  meta.maintainers = with maintainers; [ sweenu ];
+}
diff --git a/nixos/modules/services/mail/listmonk.nix b/nixos/modules/services/mail/listmonk.nix
index 8b636bd5b1f..251362fdd89 100644
--- a/nixos/modules/services/mail/listmonk.nix
+++ b/nixos/modules/services/mail/listmonk.nix
@@ -128,7 +128,7 @@ in {
           '';
         };
       };
-      package = mkPackageOption pkgs "listmonk" {};
+      package = mkPackageOptionMD pkgs "listmonk" {};
       settings = mkOption {
         type = types.submodule { freeformType = tomlFormat.type; };
         description = lib.mdDoc ''
diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix
index eeb113e204c..e11a18cc142 100644
--- a/nixos/modules/services/mail/maddy.nix
+++ b/nixos/modules/services/mail/maddy.nix
@@ -13,8 +13,6 @@ let
     # configuration here https://github.com/foxcpp/maddy/blob/master/maddy.conf
     # Do not use this in production!
 
-    tls off
-
     auth.pass_table local_authdb {
       table sql_table {
         driver sqlite3
@@ -35,6 +33,7 @@ let
       }
       optional_step file /etc/maddy/aliases
     }
+
     msgpipeline local_routing {
       destination postmaster $(local_domains) {
         modify {
@@ -215,6 +214,63 @@ in {
         '';
       };
 
+      tls = {
+        loader = mkOption {
+          type = with types; nullOr (enum [ "file" "off" ]);
+          default = "off";
+          description = lib.mdDoc ''
+            TLS certificates are obtained by modules called "certificate
+            loaders". Currently only the file loader is supported which reads
+            certificates from files specifying the options `keyPaths` and
+            `certPaths`.
+          '';
+        };
+
+        certificates = mkOption {
+          type = with types; listOf (submodule {
+            options = {
+              keyPath = mkOption {
+                type = types.path;
+                example = "/etc/ssl/mx1.example.org.key";
+                description = lib.mdDoc ''
+                  Path to the private key used for TLS.
+                '';
+              };
+              certPath = mkOption {
+                type = types.path;
+                example = "/etc/ssl/mx1.example.org.crt";
+                description = lib.mdDoc ''
+                  Path to the certificate used for TLS.
+                '';
+              };
+            };
+          });
+          default = [];
+          example = lib.literalExpression ''
+            [{
+              keyPath = "/etc/ssl/mx1.example.org.key";
+              certPath = "/etc/ssl/mx1.example.org.crt";
+            }]
+          '';
+          description = lib.mdDoc ''
+            A list of attribute sets containing paths to TLS certificates and
+            keys. Maddy will use SNI if multiple pairs are selected.
+          '';
+        };
+
+        extraConfig = mkOption {
+          type = with types; nullOr lines;
+          description = lib.mdDoc ''
+            Arguments for the specific certificate loader. Note that Maddy uses
+            secure defaults for the TLS configuration so there is no need to
+            change anything in most cases.
+            See [upstream manual](https://maddy.email/reference/tls/) for
+            available options.
+          '';
+          default = "";
+        };
+      };
+
       openFirewall = mkOption {
         type = types.bool;
         default = false;
@@ -223,22 +279,103 @@ in {
         '';
       };
 
+      ensureAccounts = mkOption {
+        type = with types; listOf str;
+        default = [];
+        description = lib.mdDoc ''
+          List of IMAP accounts which get automatically created. Note that for
+          a complete setup, user credentials for these accounts are required
+          and can be created using the `ensureCredentials` option.
+          This option does not delete accounts which are not (anymore) listed.
+        '';
+        example = [
+          "user1@localhost"
+          "user2@localhost"
+        ];
+      };
+
+      ensureCredentials = mkOption {
+        default = {};
+        description = lib.mdDoc ''
+          List of user accounts which get automatically created if they don't
+          exist yet. Note that for a complete setup, corresponding mail boxes
+          have to get created using the `ensureAccounts` option.
+          This option does not delete accounts which are not (anymore) listed.
+        '';
+        example = {
+          "user1@localhost".passwordFile = /secrets/user1-localhost;
+          "user2@localhost".passwordFile = /secrets/user2-localhost;
+        };
+        type = types.attrsOf (types.submodule {
+          options = {
+            passwordFile = mkOption {
+              type = types.path;
+              example = "/path/to/file";
+              default = null;
+              description = lib.mdDoc ''
+                Specifies the path to a file containing the
+                clear text password for the user.
+              '';
+            };
+          };
+        });
+      };
+
     };
   };
 
   config = mkIf cfg.enable {
 
+    assertions = [{
+      assertion = cfg.tls.loader == "file" -> cfg.tls.certificates != [];
+      message = ''
+        If maddy is configured to use TLS, tls.certificates with attribute sets
+        of certPath and keyPath must be provided.
+        Read more about obtaining TLS certificates here:
+        https://maddy.email/tutorials/setting-up/#tls-certificates
+      '';
+    }];
+
     systemd = {
+
       packages = [ pkgs.maddy ];
-      services.maddy = {
-        serviceConfig = {
-          User = cfg.user;
-          Group = cfg.group;
-          StateDirectory = [ "maddy" ];
+      services = {
+        maddy = {
+          serviceConfig = {
+            User = cfg.user;
+            Group = cfg.group;
+            StateDirectory = [ "maddy" ];
+          };
+          restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ];
+          wantedBy = [ "multi-user.target" ];
         };
-        restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ];
-        wantedBy = [ "multi-user.target" ];
+        maddy-ensure-accounts = {
+          script = ''
+            ${optionalString (cfg.ensureAccounts != []) ''
+              ${concatMapStrings (account: ''
+                if ! ${pkgs.maddy}/bin/maddyctl imap-acct list | grep "${account}"; then
+                  ${pkgs.maddy}/bin/maddyctl imap-acct create ${account}
+                fi
+              '') cfg.ensureAccounts}
+            ''}
+            ${optionalString (cfg.ensureCredentials != {}) ''
+              ${concatStringsSep "\n" (mapAttrsToList (name: cfg: ''
+                if ! ${pkgs.maddy}/bin/maddyctl creds list | grep "${name}"; then
+                  ${pkgs.maddy}/bin/maddyctl creds create --password $(cat ${escapeShellArg cfg.passwordFile}) ${name}
+                fi
+              '') cfg.ensureCredentials)}
+            ''}
+          '';
+          serviceConfig = {
+            Type = "oneshot";
+            User= "maddy";
+          };
+          after = [ "maddy.service" ];
+          wantedBy = [ "multi-user.target" ];
+        };
+
       };
+
     };
 
     environment.etc."maddy/maddy.conf" = {
@@ -247,6 +384,17 @@ in {
         $(primary_domain) = ${cfg.primaryDomain}
         $(local_domains) = ${toString cfg.localDomains}
         hostname ${cfg.hostname}
+
+        ${if (cfg.tls.loader == "file") then ''
+          tls file ${concatStringsSep " " (
+            map (x: x.certPath + " " + x.keyPath
+          ) cfg.tls.certificates)} ${optionalString (cfg.tls.extraConfig != "") ''
+            { ${cfg.tls.extraConfig} }
+          ''}
+        '' else if (cfg.tls.loader == "off") then ''
+          tls off
+        '' else ""}
+
         ${cfg.config}
       '';
     };
diff --git a/nixos/modules/services/mail/mailman.md b/nixos/modules/services/mail/mailman.md
new file mode 100644
index 00000000000..55b61f8a258
--- /dev/null
+++ b/nixos/modules/services/mail/mailman.md
@@ -0,0 +1,82 @@
+# Mailman {#module-services-mailman}
+
+[Mailman](https://www.list.org) is free
+software for managing electronic mail discussion and e-newsletter
+lists. Mailman and its web interface can be configured using the
+corresponding NixOS module. Note that this service is best used with
+an existing, securely configured Postfix setup, as it does not automatically configure this.
+
+## Basic usage with Postfix {#module-services-mailman-basic-usage}
+
+For a basic configuration with Postfix as the MTA, the following settings are suggested:
+```
+{ config, ... }: {
+  services.postfix = {
+    enable = true;
+    relayDomains = ["hash:/var/lib/mailman/data/postfix_domains"];
+    sslCert = config.security.acme.certs."lists.example.org".directory + "/full.pem";
+    sslKey = config.security.acme.certs."lists.example.org".directory + "/key.pem";
+    config = {
+      transport_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
+      local_recipient_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
+    };
+  };
+  services.mailman = {
+    enable = true;
+    serve.enable = true;
+    hyperkitty.enable = true;
+    webHosts = ["lists.example.org"];
+    siteOwner = "mailman@example.org";
+  };
+  services.nginx.virtualHosts."lists.example.org".enableACME = true;
+  networking.firewall.allowedTCPPorts = [ 25 80 443 ];
+}
+```
+
+DNS records will also be required:
+
+  - `AAAA` and `A` records pointing to the host in question, in order for browsers to be able to discover the address of the web server;
+  - An `MX` record pointing to a domain name at which the host is reachable, in order for other mail servers to be able to deliver emails to the mailing lists it hosts.
+
+After this has been done and appropriate DNS records have been
+set up, the Postorius mailing list manager and the Hyperkitty
+archive browser will be available at
+https://lists.example.org/. Note that this setup is not
+sufficient to deliver emails to most email providers nor to
+avoid spam -- a number of additional measures for authenticating
+incoming and outgoing mails, such as SPF, DMARC and DKIM are
+necessary, but outside the scope of the Mailman module.
+
+## Using with other MTAs {#module-services-mailman-other-mtas}
+
+Mailman also supports other MTA, though with a little bit more configuration. For example, to use Mailman with Exim, you can use the following settings:
+```
+{ config, ... }: {
+  services = {
+    mailman = {
+      enable = true;
+      siteOwner = "mailman@example.org";
+      enablePostfix = false;
+      settings.mta = {
+        incoming = "mailman.mta.exim4.LMTP";
+        outgoing = "mailman.mta.deliver.deliver";
+        lmtp_host = "localhost";
+        lmtp_port = "8024";
+        smtp_host = "localhost";
+        smtp_port = "25";
+        configuration = "python:mailman.config.exim4";
+      };
+    };
+    exim = {
+      enable = true;
+      # You can configure Exim in a separate file to reduce configuration.nix clutter
+      config = builtins.readFile ./exim.conf;
+    };
+  };
+}
+```
+
+The exim config needs some special additions to work with Mailman. Currently
+NixOS can't manage Exim config with such granularity. Please refer to
+[Mailman documentation](https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html)
+for more info on configuring Mailman for working with Exim.
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index 0ca87696b14..9273f71db7d 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -570,10 +570,14 @@ in {
           type = "normal";
           plugins = ["python3"];
           home = webEnv;
-          manage-script-name = true;
-          mount = "${cfg.serve.virtualRoot}=mailman_web.wsgi:application";
           http = "127.0.0.1:18507";
-        };
+        }
+        // (if cfg.serve.virtualRoot == "/"
+          then { module = "mailman_web.wsgi:application"; }
+          else {
+            mount = "${cfg.serve.virtualRoot}=mailman_web.wsgi:application";
+            manage-script-name = true;
+          });
         uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
       in {
         wantedBy = ["multi-user.target"];
@@ -638,7 +642,7 @@ in {
 
   meta = {
     maintainers = with lib.maintainers; [ lheckemann qyliss ma27 ];
-    doc = ./mailman.xml;
+    doc = ./mailman.md;
   };
 
 }
diff --git a/nixos/modules/services/mail/mailman.xml b/nixos/modules/services/mail/mailman.xml
deleted file mode 100644
index 27247fb064f..00000000000
--- a/nixos/modules/services/mail/mailman.xml
+++ /dev/null
@@ -1,94 +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-mailman">
-  <title>Mailman</title>
-  <para>
-    <link xlink:href="https://www.list.org">Mailman</link> is free
-    software for managing electronic mail discussion and e-newsletter
-    lists. Mailman and its web interface can be configured using the
-    corresponding NixOS module. Note that this service is best used with
-    an existing, securely configured Postfix setup, as it does not automatically configure this.
-  </para>
-
-  <section xml:id="module-services-mailman-basic-usage">
-    <title>Basic usage with Postfix</title>
-    <para>
-      For a basic configuration with Postfix as the MTA, the following settings are suggested:
-      <programlisting>{ config, ... }: {
-  services.postfix = {
-    enable = true;
-    relayDomains = ["hash:/var/lib/mailman/data/postfix_domains"];
-    sslCert = config.security.acme.certs."lists.example.org".directory + "/full.pem";
-    sslKey = config.security.acme.certs."lists.example.org".directory + "/key.pem";
-    config = {
-      transport_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
-      local_recipient_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
-    };
-  };
-  services.mailman = {
-    <link linkend="opt-services.mailman.enable">enable</link> = true;
-    <link linkend="opt-services.mailman.serve.enable">serve.enable</link> = true;
-    <link linkend="opt-services.mailman.hyperkitty.enable">hyperkitty.enable</link> = true;
-    <link linkend="opt-services.mailman.webHosts">webHosts</link> = ["lists.example.org"];
-    <link linkend="opt-services.mailman.siteOwner">siteOwner</link> = "mailman@example.org";
-  };
-  <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">services.nginx.virtualHosts."lists.example.org".enableACME</link> = true;
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 25 80 443 ];
-}</programlisting>
-    </para>
-    <para>
-      DNS records will also be required:
-      <itemizedlist>
-        <listitem><para><literal>AAAA</literal> and <literal>A</literal> records pointing to the host in question, in order for browsers to be able to discover the address of the web server;</para></listitem>
-        <listitem><para>An <literal>MX</literal> record pointing to a domain name at which the host is reachable, in order for other mail servers to be able to deliver emails to the mailing lists it hosts.</para></listitem>
-      </itemizedlist>
-    </para>
-    <para>
-      After this has been done and appropriate DNS records have been
-      set up, the Postorius mailing list manager and the Hyperkitty
-      archive browser will be available at
-      https://lists.example.org/. Note that this setup is not
-      sufficient to deliver emails to most email providers nor to
-      avoid spam -- a number of additional measures for authenticating
-      incoming and outgoing mails, such as SPF, DMARC and DKIM are
-      necessary, but outside the scope of the Mailman module.
-    </para>
-  </section>
-  <section xml:id="module-services-mailman-other-mtas">
-    <title>Using with other MTAs</title>
-    <para>
-      Mailman also supports other MTA, though with a little bit more configuration. For example, to use Mailman with Exim, you can use the following settings:
-      <programlisting>{ config, ... }: {
-  services = {
-    mailman = {
-      enable = true;
-      siteOwner = "mailman@example.org";
-      <link linkend="opt-services.mailman.enablePostfix">enablePostfix</link> = false;
-      settings.mta = {
-        incoming = "mailman.mta.exim4.LMTP";
-        outgoing = "mailman.mta.deliver.deliver";
-        lmtp_host = "localhost";
-        lmtp_port = "8024";
-        smtp_host = "localhost";
-        smtp_port = "25";
-        configuration = "python:mailman.config.exim4";
-      };
-    };
-    exim = {
-      enable = true;
-      # You can configure Exim in a separate file to reduce configuration.nix clutter
-      config = builtins.readFile ./exim.conf;
-    };
-  };
-}</programlisting>
-    </para>
-    <para>
-      The exim config needs some special additions to work with Mailman. Currently
-      NixOS can't manage Exim config with such granularity. Please refer to
-      <link xlink:href="https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html">Mailman documentation</link>
-      for more info on configuring Mailman for working with Exim.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index d01734d61e8..23c47aaca7e 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -234,7 +234,7 @@ let
 
   headerChecks = concatStringsSep "\n" (map (x: "${x.pattern} ${x.action}") cfg.headerChecks) + cfg.extraHeaderChecks;
 
-  aliases = let separator = if cfg.aliasMapType == "hash" then ":" else ""; in
+  aliases = let separator = optionalString (cfg.aliasMapType == "hash") ":"; in
     optionalString (cfg.postmasterAlias != "") ''
       postmaster${separator} ${cfg.postmasterAlias}
     ''
@@ -809,7 +809,7 @@ in
       // optionalAttrs (cfg.relayHost != "") { relayhost = if cfg.lookupMX
                                                            then "${cfg.relayHost}:${toString cfg.relayPort}"
                                                            else "[${cfg.relayHost}]:${toString cfg.relayPort}"; }
-      // optionalAttrs config.networking.enableIPv6 { inet_protocols = mkDefault "all"; }
+      // optionalAttrs (!config.networking.enableIPv6) { inet_protocols = mkDefault "ipv4"; }
       // optionalAttrs (cfg.networks != null) { mynetworks = cfg.networks; }
       // optionalAttrs (cfg.networksStyle != "") { mynetworks_style = cfg.networksStyle; }
       // optionalAttrs (cfg.hostname != "") { myhostname = cfg.hostname; }
diff --git a/nixos/modules/services/mail/public-inbox.nix b/nixos/modules/services/mail/public-inbox.nix
index ab7ff5f726a..9cd6726e6cb 100644
--- a/nixos/modules/services/mail/public-inbox.nix
+++ b/nixos/modules/services/mail/public-inbox.nix
@@ -275,7 +275,11 @@ in
           default = {};
           description = lib.mdDoc "public inboxes";
           type = types.submodule {
-            freeformType = with types; /*inbox name*/attrsOf (/*inbox option name*/attrsOf /*inbox option value*/iniAtom);
+            # Keeping in line with the tradition of unnecessarily specific types, allow users to set
+            # freeform settings either globally under the `publicinbox` section, or for specific
+            # inboxes through additional nesting.
+            freeformType = with types; attrsOf (oneOf [ iniAtom (attrsOf iniAtom) ]);
+
             options.css = mkOption {
               type = with types; listOf str;
               default = [];
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index e05820fb87c..b9cf526b0bb 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -7,7 +7,7 @@ let
   fpm = config.services.phpfpm.pools.roundcube;
   localDB = cfg.database.host == "localhost";
   user = cfg.database.username;
-  phpWithPspell = pkgs.php80.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
+  phpWithPspell = pkgs.php81.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
 in
 {
   options.services.roundcube = {
@@ -70,7 +70,12 @@ in
       };
       passwordFile = mkOption {
         type = types.str;
-        description = lib.mdDoc "Password file for the postgresql connection. Must be readable by user `nginx`. Ignored if `database.host` is set to `localhost`, as peer authentication will be used.";
+        description = lib.mdDoc ''
+          Password file for the postgresql connection.
+          Must be formated according to PostgreSQL .pgpass standard (see https://www.postgresql.org/docs/current/libpq-pgpass.html)
+          but only one line, no comments and readable by user `nginx`.
+          Ignored if `database.host` is set to `localhost`, as peer authentication will be used.
+        '';
       };
       dbname = mkOption {
         type = types.str;
@@ -123,7 +128,13 @@ in
     environment.etc."roundcube/config.inc.php".text = ''
       <?php
 
-      ${lib.optionalString (!localDB) "$password = file_get_contents('${cfg.database.passwordFile}');"}
+      ${lib.optionalString (!localDB) ''
+        $password = file('${cfg.database.passwordFile}')[0];
+        $password = preg_split('~\\\\.(*SKIP)(*FAIL)|\:~s', $password);
+        $password = end($password);
+        $password = str_replace("\\:", ":", $password);
+        $password = str_replace("\\\\", "\\", $password);
+      ''}
 
       $config = array();
       $config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
@@ -132,6 +143,8 @@ in
       $config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
       $config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
       $config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
+      # Roundcube uses PHP-FPM which has `PrivateTmp = true;`
+      $config['temp_dir'] = '/tmp';
       $config['enable_spellcheck'] = ${if cfg.dicts == [] then "false" else "true"};
       # by default, spellchecking uses a third-party cloud services
       $config['spellcheck_engine'] = 'pspell';
@@ -150,9 +163,13 @@ in
             root = cfg.package;
             index = "index.php";
             extraConfig = ''
-              location ~* \.php$ {
+              location ~* \.php(/|$) {
                 fastcgi_split_path_info ^(.+\.php)(/.+)$;
                 fastcgi_pass unix:${fpm.socket};
+
+                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+                fastcgi_param PATH_INFO       $fastcgi_path_info;
+
                 include ${config.services.nginx.package}/conf/fastcgi_params;
                 include ${pkgs.nginx}/conf/fastcgi.conf;
               }
@@ -217,6 +234,7 @@ in
         path = [ config.services.postgresql.package ];
       })
       {
+        after = [ "network-online.target" ];
         wantedBy = [ "multi-user.target" ];
         script = let
           psql = "${lib.optionalString (!localDB) "PGPASSFILE=${cfg.database.passwordFile}"} ${pkgs.postgresql}/bin/psql ${lib.optionalString (!localDB) "-h ${cfg.database.host} -U ${cfg.database.username} "} ${cfg.database.dbname}";
diff --git a/nixos/modules/services/mail/zeyple.nix b/nixos/modules/services/mail/zeyple.nix
new file mode 100644
index 00000000000..e7f9ddd92dc
--- /dev/null
+++ b/nixos/modules/services/mail/zeyple.nix
@@ -0,0 +1,125 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.zeyple;
+  ini = pkgs.formats.ini { };
+
+  gpgHome = pkgs.runCommand "zeyple-gpg-home" { } ''
+    mkdir -p $out
+    for file in ${lib.concatStringsSep " " cfg.keys}; do
+      ${config.programs.gnupg.package}/bin/gpg --homedir="$out" --import "$file"
+    done
+
+    # Remove socket files
+    rm -f $out/S.*
+  '';
+in {
+  options.services.zeyple = {
+    enable = mkEnableOption (lib.mdDoc "Zeyple, an utility program to automatically encrypt outgoing emails with GPG");
+
+    user = mkOption {
+      type = types.str;
+      default = "zeyple";
+      description = lib.mdDoc ''
+        User to run Zeyple as.
+
+        ::: {.note}
+        If left as the default value this user will automatically be created
+        on system activation, otherwise the sysadmin is responsible for
+        ensuring the user exists.
+        :::
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "zeyple";
+      description = lib.mdDoc ''
+        Group to use to run Zeyple.
+
+        ::: {.note}
+        If left as the default value this group will automatically be created
+        on system activation, otherwise the sysadmin is responsible for
+        ensuring the user exists.
+        :::
+      '';
+    };
+
+    settings = mkOption {
+      type = ini.type;
+      default = { };
+      description = lib.mdDoc ''
+        Zeyple configuration. refer to
+        <https://github.com/infertux/zeyple/blob/master/zeyple/zeyple.conf.example>
+        for details on supported values.
+      '';
+    };
+
+    keys = mkOption {
+      type = with types; listOf path;
+      description = lib.mdDoc "List of public key files that will be imported by gpg.";
+    };
+
+    rotateLogs = mkOption {
+      type = types.bool;
+      default = true;
+      description = lib.mdDoc "Whether to enable rotation of log files.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    users.groups = optionalAttrs (cfg.group == "zeyple") { "${cfg.group}" = { }; };
+    users.users = optionalAttrs (cfg.user == "zeyple") {
+      "${cfg.user}" = {
+        isSystemUser = true;
+        group = cfg.group;
+      };
+    };
+
+    services.zeyple.settings = {
+      zeyple = mapAttrs (name: mkDefault) {
+        log_file = "/var/log/zeyple/zeyple.log";
+        force_encrypt = true;
+      };
+
+      gpg = mapAttrs (name: mkDefault) { home = "${gpgHome}"; };
+
+      relay = mapAttrs (name: mkDefault) {
+        host = "localhost";
+        port = 10026;
+      };
+    };
+
+    environment.etc."zeyple.conf".source = ini.generate "zeyple.conf" cfg.settings;
+
+    systemd.tmpfiles.rules = [ "f '${cfg.settings.zeyple.log_file}' 0600 ${cfg.user} ${cfg.group} - -" ];
+    services.logrotate = mkIf cfg.rotateLogs {
+      enable = true;
+      settings.zeyple = {
+        files = cfg.settings.zeyple.log_file;
+        frequency = "weekly";
+        rotate = 5;
+        compress = true;
+        copytruncate = true;
+      };
+    };
+
+    services.postfix.extraMasterConf = ''
+      zeyple    unix  -       n       n       -       -       pipe
+        user=${cfg.user} argv=${pkgs.zeyple}/bin/zeyple ''${recipient}
+
+      localhost:${toString cfg.settings.relay.port} inet  n       -       n       -       10      smtpd
+        -o content_filter=
+        -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters
+        -o smtpd_helo_restrictions=
+        -o smtpd_client_restrictions=
+        -o smtpd_sender_restrictions=
+        -o smtpd_recipient_restrictions=permit_mynetworks,reject
+        -o mynetworks=127.0.0.0/8,[::1]/128
+        -o smtpd_authorized_xforward_hosts=127.0.0.0/8,[::1]/128
+    '';
+
+    services.postfix.extraConfig = "content_filter = zeyple";
+  };
+}
diff --git a/nixos/modules/services/matrix/appservice-discord.nix b/nixos/modules/services/matrix/appservice-discord.nix
index 15f0f0cc0cd..f579c2529c0 100644
--- a/nixos/modules/services/matrix/appservice-discord.nix
+++ b/nixos/modules/services/matrix/appservice-discord.nix
@@ -5,7 +5,6 @@ with lib;
 let
   dataDir = "/var/lib/matrix-appservice-discord";
   registrationFile = "${dataDir}/discord-registration.yaml";
-  appDir = "${pkgs.matrix-appservice-discord}/${pkgs.matrix-appservice-discord.passthru.nodeAppDir}";
   cfg = config.services.matrix-appservice-discord;
   opt = options.services.matrix-appservice-discord;
   # TODO: switch to configGen.json once RFC42 is implemented
@@ -16,6 +15,15 @@ in {
     services.matrix-appservice-discord = {
       enable = mkEnableOption (lib.mdDoc "a bridge between Matrix and Discord");
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.matrix-appservice-discord;
+        defaultText = literalExpression "pkgs.matrix-appservice-discord";
+        description = lib.mdDoc ''
+          Which package of matrix-appservice-discord to use.
+        '';
+      };
+
       settings = mkOption rec {
         # TODO: switch to types.config.json as prescribed by RFC42 once it's implemented
         type = types.attrs;
@@ -114,7 +122,7 @@ in {
 
       preStart = ''
         if [ ! -f '${registrationFile}' ]; then
-          ${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
+          ${cfg.package}/bin/matrix-appservice-discord \
             --generate-registration \
             --url=${escapeShellArg cfg.url} \
             ${optionalString (cfg.localpart != null) "--localpart=${escapeShellArg cfg.localpart}"} \
@@ -135,13 +143,13 @@ in {
 
         DynamicUser = true;
         PrivateTmp = true;
-        WorkingDirectory = appDir;
+        WorkingDirectory = "${cfg.package}/${cfg.package.passthru.nodeAppDir}";
         StateDirectory = baseNameOf dataDir;
         UMask = "0027";
         EnvironmentFile = cfg.environmentFile;
 
         ExecStart = ''
-          ${pkgs.matrix-appservice-discord}/bin/matrix-appservice-discord \
+          ${cfg.package}/bin/matrix-appservice-discord \
             --file='${registrationFile}' \
             --config='${settingsFile}' \
             --port='${toString cfg.port}'
diff --git a/nixos/modules/services/matrix/dendrite.nix b/nixos/modules/services/matrix/dendrite.nix
index a5fea3da484..244c15fbf7a 100644
--- a/nixos/modules/services/matrix/dendrite.nix
+++ b/nixos/modules/services/matrix/dendrite.nix
@@ -159,6 +159,15 @@ in
             '';
           };
         };
+        options.relay_api.database = {
+          connection_string = lib.mkOption {
+            type = lib.types.str;
+            default = "file:relayapi.db";
+            description = lib.mdDoc ''
+              Database for the Relay Server.
+            '';
+          };
+        };
         options.media_api = {
           database = {
             connection_string = lib.mkOption {
@@ -288,13 +297,13 @@ in
         LimitNOFILE = 65535;
         EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
         LoadCredential = cfg.loadCredential;
-        ExecStartPre = ''
+        ExecStartPre = [''
           ${pkgs.envsubst}/bin/envsubst \
             -i ${configurationYaml} \
             -o /run/dendrite/dendrite.yaml
-        '';
+        ''];
         ExecStart = lib.strings.concatStringsSep " " ([
-          "${pkgs.dendrite}/bin/dendrite-monolith-server"
+          "${pkgs.dendrite}/bin/dendrite"
           "--config /run/dendrite/dendrite.yaml"
         ] ++ lib.optionals (cfg.httpPort != null) [
           "--http-bind-address :${builtins.toString cfg.httpPort}"
diff --git a/nixos/modules/services/matrix/mautrix-facebook.nix b/nixos/modules/services/matrix/mautrix-facebook.nix
index e74f25df764..e995f1aecf2 100644
--- a/nixos/modules/services/matrix/mautrix-facebook.nix
+++ b/nixos/modules/services/matrix/mautrix-facebook.nix
@@ -96,7 +96,7 @@ in {
         type = types.nullOr types.path;
         default = null;
         description = lib.mdDoc ''
-          File containing environment variables to be passed to the mautrix-telegram service.
+          File containing environment variables to be passed to the mautrix-facebook service.
 
           Any config variable can be overridden by setting `MAUTRIX_FACEBOOK_SOME_KEY` to override the `some.key` variable.
         '';
diff --git a/nixos/modules/services/matrix/mautrix-telegram.nix b/nixos/modules/services/matrix/mautrix-telegram.nix
index 5a632fd27e8..b64cc71d987 100644
--- a/nixos/modules/services/matrix/mautrix-telegram.nix
+++ b/nixos/modules/services/matrix/mautrix-telegram.nix
@@ -137,7 +137,7 @@ in {
       wantedBy = [ "multi-user.target" ];
       wants = [ "network-online.target" ] ++ cfg.serviceDependencies;
       after = [ "network-online.target" ] ++ cfg.serviceDependencies;
-      path = [ pkgs.lottieconverter ];
+      path = [ pkgs.lottieconverter pkgs.ffmpeg-full ];
 
       # mautrix-telegram tries to generate a dotfile in the home directory of
       # the running user if using a postgresql database:
diff --git a/nixos/modules/services/matrix/mjolnir.md b/nixos/modules/services/matrix/mjolnir.md
new file mode 100644
index 00000000000..f6994eeb8fa
--- /dev/null
+++ b/nixos/modules/services/matrix/mjolnir.md
@@ -0,0 +1,110 @@
+# Mjolnir (Matrix Moderation Tool) {#module-services-mjolnir}
+
+This chapter will show you how to set up your own, self-hosted
+[Mjolnir](https://github.com/matrix-org/mjolnir) instance.
+
+As an all-in-one moderation tool, it can protect your server from
+malicious invites, spam messages, and whatever else you don't want.
+In addition to server-level protection, Mjolnir is great for communities
+wanting to protect their rooms without having to use their personal
+accounts for moderation.
+
+The bot by default includes support for bans, redactions, anti-spam,
+server ACLs, room directory changes, room alias transfers, account
+deactivation, room shutdown, and more.
+
+See the [README](https://github.com/matrix-org/mjolnir#readme)
+page and the [Moderator's guide](https://github.com/matrix-org/mjolnir/blob/main/docs/moderators.md)
+for additional instructions on how to setup and use Mjolnir.
+
+For [additional settings](#opt-services.mjolnir.settings)
+see [the default configuration](https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml).
+
+## Mjolnir Setup {#module-services-mjolnir-setup}
+
+First create a new Room which will be used as a management room for Mjolnir. In
+this room, Mjolnir will log possible errors and debugging information. You'll
+need to set this Room-ID in [services.mjolnir.managementRoom](#opt-services.mjolnir.managementRoom).
+
+Next, create a new user for Mjolnir on your homeserver, if not present already.
+
+The Mjolnir Matrix user expects to be free of any rate limiting.
+See [Synapse #6286](https://github.com/matrix-org/synapse/issues/6286)
+for an example on how to achieve this.
+
+If you want Mjolnir to be able to deactivate users, move room aliases, shutdown rooms, etc.
+you'll need to make the Mjolnir user a Matrix server admin.
+
+Now invite the Mjolnir user to the management room.
+
+It is recommended to use [Pantalaimon](https://github.com/matrix-org/pantalaimon),
+so your management room can be encrypted. This also applies if you are looking to moderate an encrypted room.
+
+To enable the Pantalaimon E2E Proxy for mjolnir, enable
+[services.mjolnir.pantalaimon](#opt-services.mjolnir.pantalaimon.enable). This will
+autoconfigure a new Pantalaimon instance, which will connect to the homeserver
+set in [services.mjolnir.homeserverUrl](#opt-services.mjolnir.homeserverUrl) and Mjolnir itself
+will be configured to connect to the new Pantalaimon instance.
+
+```
+{
+  services.mjolnir = {
+    enable = true;
+    homeserverUrl = "https://matrix.domain.tld";
+    pantalaimon = {
+       enable = true;
+       username = "mjolnir";
+       passwordFile = "/run/secrets/mjolnir-password";
+    };
+    protectedRooms = [
+      "https://matrix.to/#/!xxx:domain.tld"
+    ];
+    managementRoom = "!yyy:domain.tld";
+  };
+}
+```
+
+### Element Matrix Services (EMS) {#module-services-mjolnir-setup-ems}
+
+If you are using a managed ["Element Matrix Services (EMS)"](https://ems.element.io/)
+server, you will need to consent to the terms and conditions. Upon startup, an error
+log entry with a URL to the consent page will be generated.
+
+## Synapse Antispam Module {#module-services-mjolnir-matrix-synapse-antispam}
+
+A Synapse module is also available to apply the same rulesets the bot
+uses across an entire homeserver.
+
+To use the Antispam Module, add `matrix-synapse-plugins.matrix-synapse-mjolnir-antispam`
+to the Synapse plugin list and enable the `mjolnir.Module` module.
+
+```
+{
+  services.matrix-synapse = {
+    plugins = with pkgs; [
+      matrix-synapse-plugins.matrix-synapse-mjolnir-antispam
+    ];
+    extraConfig = ''
+      modules:
+        - module: mjolnir.Module
+          config:
+            # Prevent servers/users in the ban lists from inviting users on this
+            # server to rooms. Default true.
+            block_invites: true
+            # Flag messages sent by servers/users in the ban lists as spam. Currently
+            # this means that spammy messages will appear as empty to users. Default
+            # false.
+            block_messages: false
+            # Remove users from the user directory search by filtering matrix IDs and
+            # display names by the entries in the user ban list. Default false.
+            block_usernames: false
+            # The room IDs of the ban lists to honour. Unlike other parts of Mjolnir,
+            # this list cannot be room aliases or permalinks. This server is expected
+            # to already be joined to the room - Mjolnir will not automatically join
+            # these rooms.
+            ban_lists:
+              - "!roomid:example.org"
+    '';
+  };
+}
+```
diff --git a/nixos/modules/services/matrix/mjolnir.nix b/nixos/modules/services/matrix/mjolnir.nix
index cbf7b93329d..b6a3e5e8c73 100644
--- a/nixos/modules/services/matrix/mjolnir.nix
+++ b/nixos/modules/services/matrix/mjolnir.nix
@@ -236,7 +236,7 @@ in
   };
 
   meta = {
-    doc = ./mjolnir.xml;
+    doc = ./mjolnir.md;
     maintainers = with maintainers; [ jojosch ];
   };
 }
diff --git a/nixos/modules/services/matrix/mjolnir.xml b/nixos/modules/services/matrix/mjolnir.xml
deleted file mode 100644
index b07abe33979..00000000000
--- a/nixos/modules/services/matrix/mjolnir.xml
+++ /dev/null
@@ -1,134 +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-mjolnir">
- <title>Mjolnir (Matrix Moderation Tool)</title>
- <para>
-  This chapter will show you how to set up your own, self-hosted
-  <link xlink:href="https://github.com/matrix-org/mjolnir">Mjolnir</link>
-  instance.
- </para>
- <para>
-  As an all-in-one moderation tool, it can protect your server from
-  malicious invites, spam messages, and whatever else you don't want.
-  In addition to server-level protection, Mjolnir is great for communities
-  wanting to protect their rooms without having to use their personal
-  accounts for moderation.
- </para>
- <para>
-  The bot by default includes support for bans, redactions, anti-spam,
-  server ACLs, room directory changes, room alias transfers, account
-  deactivation, room shutdown, and more.
- </para>
- <para>
-  See the <link xlink:href="https://github.com/matrix-org/mjolnir#readme">README</link>
-  page and the <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/docs/moderators.md">Moderator's guide</link>
-  for additional instructions on how to setup and use Mjolnir.
- </para>
- <para>
-  For <link linkend="opt-services.mjolnir.settings">additional settings</link>
-  see <link xlink:href="https://github.com/matrix-org/mjolnir/blob/main/config/default.yaml">the default configuration</link>.
- </para>
- <section xml:id="module-services-mjolnir-setup">
-  <title>Mjolnir Setup</title>
-  <para>
-   First create a new Room which will be used as a management room for Mjolnir. In
-   this room, Mjolnir will log possible errors and debugging information. You'll
-   need to set this Room-ID in <link linkend="opt-services.mjolnir.managementRoom">services.mjolnir.managementRoom</link>.
-  </para>
-  <para>
-   Next, create a new user for Mjolnir on your homeserver, if not present already.
-  </para>
-  <para>
-   The Mjolnir Matrix user expects to be free of any rate limiting.
-   See <link xlink:href="https://github.com/matrix-org/synapse/issues/6286">Synapse #6286</link>
-   for an example on how to achieve this.
-  </para>
-  <para>
-   If you want Mjolnir to be able to deactivate users, move room aliases, shutdown rooms, etc.
-   you'll need to make the Mjolnir user a Matrix server admin.
-  </para>
-  <para>
-   Now invite the Mjolnir user to the management room.
-  </para>
-  <para>
-   It is recommended to use <link xlink:href="https://github.com/matrix-org/pantalaimon">Pantalaimon</link>,
-   so your management room can be encrypted. This also applies if you are looking to moderate an encrypted room.
-  </para>
-  <para>
-   To enable the Pantalaimon E2E Proxy for mjolnir, enable
-   <link linkend="opt-services.mjolnir.pantalaimon.enable">services.mjolnir.pantalaimon</link>. This will
-   autoconfigure a new Pantalaimon instance, which will connect to the homeserver
-   set in <link linkend="opt-services.mjolnir.homeserverUrl">services.mjolnir.homeserverUrl</link> and Mjolnir itself
-   will be configured to connect to the new Pantalaimon instance.
-  </para>
-<programlisting>
-{
-  services.mjolnir = {
-    enable = true;
-    <link linkend="opt-services.mjolnir.homeserverUrl">homeserverUrl</link> = "https://matrix.domain.tld";
-    <link linkend="opt-services.mjolnir.pantalaimon">pantalaimon</link> = {
-       <link linkend="opt-services.mjolnir.pantalaimon.enable">enable</link> = true;
-       <link linkend="opt-services.mjolnir.pantalaimon.username">username</link> = "mjolnir";
-       <link linkend="opt-services.mjolnir.pantalaimon.passwordFile">passwordFile</link> = "/run/secrets/mjolnir-password";
-    };
-    <link linkend="opt-services.mjolnir.protectedRooms">protectedRooms</link> = [
-      "https://matrix.to/#/!xxx:domain.tld"
-    ];
-    <link linkend="opt-services.mjolnir.managementRoom">managementRoom</link> = "!yyy:domain.tld";
-  };
-}
-</programlisting>
- <section xml:id="module-services-mjolnir-setup-ems">
-  <title>Element Matrix Services (EMS)</title>
-  <para>
-   If you are using a managed <link xlink:href="https://ems.element.io/">"Element Matrix Services (EMS)"</link>
-   server, you will need to consent to the terms and conditions. Upon startup, an error
-   log entry with a URL to the consent page will be generated.
-  </para>
- </section>
- </section>
-
- <section xml:id="module-services-mjolnir-matrix-synapse-antispam">
-  <title>Synapse Antispam Module</title>
-  <para>
-   A Synapse module is also available to apply the same rulesets the bot
-   uses across an entire homeserver.
-  </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.Module</literal> module.
-  </para>
-<programlisting>
-{
-  services.matrix-synapse = {
-    plugins = with pkgs; [
-      matrix-synapse-plugins.matrix-synapse-mjolnir-antispam
-    ];
-    extraConfig = ''
-      modules:
-        - module: mjolnir.Module
-          config:
-            # Prevent servers/users in the ban lists from inviting users on this
-            # server to rooms. Default true.
-            block_invites: true
-            # Flag messages sent by servers/users in the ban lists as spam. Currently
-            # this means that spammy messages will appear as empty to users. Default
-            # false.
-            block_messages: false
-            # Remove users from the user directory search by filtering matrix IDs and
-            # display names by the entries in the user ban list. Default false.
-            block_usernames: false
-            # The room IDs of the ban lists to honour. Unlike other parts of Mjolnir,
-            # this list cannot be room aliases or permalinks. This server is expected
-            # to already be joined to the room - Mjolnir will not automatically join
-            # these rooms.
-            ban_lists:
-              - "!roomid:example.org"
-    '';
-  };
-}
-</programlisting>
- </section>
-</chapter>
diff --git a/nixos/modules/services/misc/mx-puppet-discord.nix b/nixos/modules/services/matrix/mx-puppet-discord.nix
index 36c9f8b122e..36c9f8b122e 100644
--- a/nixos/modules/services/misc/mx-puppet-discord.nix
+++ b/nixos/modules/services/matrix/mx-puppet-discord.nix
diff --git a/nixos/modules/services/matrix/synapse.md b/nixos/modules/services/matrix/synapse.md
new file mode 100644
index 00000000000..cad91ebf58d
--- /dev/null
+++ b/nixos/modules/services/matrix/synapse.md
@@ -0,0 +1,213 @@
+# Matrix {#module-services-matrix}
+
+[Matrix](https://matrix.org/) 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.
+
+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
+[Try Matrix Now!](https://matrix.org/docs/projects/try-matrix-now.html)
+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.
+
+## Synapse Homeserver {#module-services-matrix-synapse}
+
+[Synapse](https://github.com/matrix-org/synapse) 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 `example.org` domain, served from
+the host `myhostname.example.org`. For more information,
+please refer to the
+[installation instructions of Synapse](https://matrix-org.github.io/synapse/latest/setup/installation.html) .
+```
+{ pkgs, lib, config, ... }:
+let
+  fqdn = "${config.networking.hostName}.${config.networking.domain}";
+  clientConfig."m.homeserver".base_url = "https://${fqdn}";
+  serverConfig."m.server" = "${fqdn}:443";
+  mkWellKnown = data: ''
+    add_header Content-Type application/json;
+    add_header Access-Control-Allow-Origin *;
+    return 200 '${builtins.toJSON data}';
+  '';
+in {
+  networking.hostName = "myhostname";
+  networking.domain = "example.org";
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+
+  services.postgresql.enable = true;
+  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 = {
+    enable = true;
+    recommendedTlsSettings = true;
+    recommendedOptimisation = true;
+    recommendedGzipSettings = true;
+    recommendedProxySettings = true;
+    virtualHosts = {
+      # If the A and AAAA DNS records on example.org do not point on the same host as the
+      # records for myhostname.example.org, you can easily move the /.well-known
+      # virtualHost section of the code to the host that is serving example.org, while
+      # the rest stays on myhostname.example.org with no other changes required.
+      # This pattern also allows to seamlessly move the homeserver from
+      # myhostname.example.org to myotherhost.example.org by only changing the
+      # /.well-known redirection target.
+      "${config.networking.domain}" = {
+        enableACME = true;
+        forceSSL = true;
+        # This section is not needed if the server_name of matrix-synapse is equal to
+        # the domain (i.e. example.org from @foo:example.org) and the federation port
+        # is 8448.
+        # Further reference can be found in the docs about delegation under
+        # https://matrix-org.github.io/synapse/latest/delegate.html
+        locations."= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig;
+        # This is usually needed for homeserver discovery (from e.g. other Matrix clients).
+        # Further reference can be found in the upstream docs at
+        # https://spec.matrix.org/latest/client-server-api/#getwell-knownmatrixclient
+        locations."= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig;
+      };
+      "${fqdn}" = {
+        enableACME = true;
+        forceSSL = true;
+        # It's also possible to do a redirect here or something else, this vhost is not
+        # needed for Matrix. It's recommended though to *not put* element
+        # here, see also the section about Element.
+        locations."/".extraConfig = ''
+          return 404;
+        '';
+        # Forward all Matrix API calls to the synapse Matrix homeserver. A trailing slash
+        # *must not* be used here.
+        locations."/_matrix".proxyPass = "http://[::1]:8008";
+        # Forward requests for e.g. SSO and password-resets.
+        locations."/_synapse/client".proxyPass = "http://[::1]:8008";
+      };
+    };
+  };
+
+  services.matrix-synapse = {
+    enable = true;
+    settings.server_name = config.networking.domain;
+    settings.listeners = [
+      { port = 8008;
+        bind_addresses = [ "::1" ];
+        type = "http";
+        tls = false;
+        x_forwarded = true;
+        resources = [ {
+          names = [ "client" "federation" ];
+          compress = true;
+        } ];
+      }
+    ];
+  };
+}
+```
+
+## Registering Matrix users {#module-services-matrix-register-users}
+
+If you want to run a server with public registration by anybody, you can
+then enable `services.matrix-synapse.settings.enable_registration = true;`.
+Otherwise, or you can generate a registration secret with
+{command}`pwgen -s 64 1` and set it with
+[](#opt-services.matrix-synapse.settings.registration_shared_secret).
+To create a new user or admin, run the following after you have set the secret
+and have rebuilt NixOS:
+```ShellSession
+$ nix-shell -p matrix-synapse
+$ register_new_matrix_user -k your-registration-shared-secret http://localhost:8008
+New user localpart: your-username
+Password:
+Confirm password:
+Make admin [no]:
+Success!
+```
+In the example, this would create a user with the Matrix Identifier
+`@your-username:example.org`.
+
+::: {.warning}
+When using [](#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:
+
+  - Create a file with the following contents:
+
+    ```
+    registration_shared_secret: your-very-secret-secret
+    ```
+  - Deploy the file with a secret-manager such as
+    [{option}`deployment.keys`](https://nixops.readthedocs.io/en/latest/overview.html#managing-keys)
+    from {manpage}`nixops(1)` or [sops-nix](https://github.com/Mic92/sops-nix/) to
+    e.g. {file}`/run/secrets/matrix-shared-secret` and ensure that it's readable
+    by `matrix-synapse`.
+  - Include the file like this in your configuration:
+
+    ```
+    {
+      services.matrix-synapse.extraConfigFiles = [
+        "/run/secrets/matrix-shared-secret"
+      ];
+    }
+    ```
+:::
+
+::: {.note}
+It's also possible to user alternative authentication mechanism such as
+[LDAP (via `matrix-synapse-ldap3`)](https://github.com/matrix-org/matrix-synapse-ldap3)
+or [OpenID](https://matrix-org.github.io/synapse/latest/openid.html).
+:::
+
+## Element (formerly known as Riot) Web Client {#module-services-matrix-element-web}
+
+[Element Web](https://github.com/vector-im/riot-web/) 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
+[Element introductory blog post](https://element.io/blog/welcome-to-element/)
+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
+`https://element.myhostname.example.org` and
+`https://element.example.org`. Alternatively, you can use the hosted
+copy at <https://app.element.io/>,
+or use other web clients or native client applications. Due to the
+`/.well-known` urls set up done above, many clients should
+fill in the required connection details automatically when you enter your
+Matrix Identifier. See
+[Try Matrix Now!](https://matrix.org/docs/projects/try-matrix-now.html)
+for a list of existing clients and their supported featureset.
+```
+{
+  services.nginx.virtualHosts."element.${fqdn}" = {
+    enableACME = true;
+    forceSSL = true;
+    serverAliases = [
+      "element.${config.networking.domain}"
+    ];
+
+    root = pkgs.element-web.override {
+      conf = {
+        default_server_config = clientConfig; # see `clientConfig` from the snippet above.
+      };
+    };
+  };
+}
+```
+
+::: {.note}
+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
+`myhostname.example.org` virtualHost to also serve Element,
+but instead serve it on a different subdomain, like
+`element.example.org` in the example. See the
+[Element Important Security Notes](https://github.com/vector-im/element-web/tree/v1.10.0#important-security-notes)
+for more information on this subject.
+:::
diff --git a/nixos/modules/services/matrix/synapse.nix b/nixos/modules/services/matrix/synapse.nix
index b9b581acb34..2a4104a4ec2 100644
--- a/nixos/modules/services/matrix/synapse.nix
+++ b/nixos/modules/services/matrix/synapse.nix
@@ -60,7 +60,7 @@ in {
     '')
     (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>
+      <nixpkgs/nixos/tests/matrix/synapse.nix>
     '')
     (mkRemovedOptionModule [ "services" "matrix-synapse" "web_client" ] "")
     (mkRemovedOptionModule [ "services" "matrix-synapse" "room_invite_state_types" ] ''
@@ -507,6 +507,12 @@ in {
                 sqlite3 = null;
                 psycopg2 = "matrix-synapse";
               }.${cfg.settings.database.name};
+              defaultText = lib.literalExpression ''
+                {
+                  sqlite3 = null;
+                  psycopg2 = "matrix-synapse";
+                }.''${cfg.settings.database.name};
+              '';
               description = lib.mdDoc ''
                 Username to connect with psycopg2, set to null
                 when using sqlite3.
@@ -705,7 +711,7 @@ in {
 
             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>
+              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.
 
@@ -749,8 +755,8 @@ in {
         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
+          chown matrix-synapse:matrix-synapse ${cfg.settings.signing_key_path}
+          chmod 0600 ${cfg.settings.signing_key_path}
         '')) ];
         ExecStart = ''
           ${cfg.package}/bin/synapse_homeserver \
@@ -795,7 +801,7 @@ in {
 
   meta = {
     buildDocsInSandbox = false;
-    doc = ./synapse.xml;
+    doc = ./synapse.md;
     maintainers = teams.matrix.members;
   };
 
diff --git a/nixos/modules/services/matrix/synapse.xml b/nixos/modules/services/matrix/synapse.xml
deleted file mode 100644
index 40ad72173a5..00000000000
--- a/nixos/modules/services/matrix/synapse.xml
+++ /dev/null
@@ -1,276 +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://matrix-org.github.io/synapse/latest/setup/installation.html">
-   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/atuin.nix b/nixos/modules/services/misc/atuin.nix
index c94852e3aad..c603042fb30 100644
--- a/nixos/modules/services/misc/atuin.nix
+++ b/nixos/modules/services/misc/atuin.nix
@@ -8,7 +8,7 @@ in
 {
   options = {
     services.atuin = {
-      enable = mkEnableOption (mdDoc "Enable server for shell history sync with atuin.");
+      enable = mkEnableOption (mdDoc "Enable server for shell history sync with atuin");
 
       openRegistration = mkOption {
         type = types.bool;
@@ -28,6 +28,12 @@ in
         description = mdDoc "The host address the atuin server should listen on.";
       };
 
+      maxHistoryLength = mkOption {
+        type = types.int;
+        default = 8192;
+        description = mdDoc "The max length of each history item the atuin server should store.";
+      };
+
       port = mkOption {
         type = types.port;
         default = 8888;
@@ -72,6 +78,7 @@ in
       environment = {
         ATUIN_HOST = cfg.host;
         ATUIN_PORT = toString cfg.port;
+        ATUIN_MAX_HISTORY_LENGTH = toString cfg.maxHistoryLength;
         ATUIN_OPEN_REGISTRATION = boolToString cfg.openRegistration;
         ATUIN_DB_URI = "postgresql:///atuin";
         ATUIN_PATH = cfg.path;
diff --git a/nixos/modules/services/misc/autorandr.nix b/nixos/modules/services/misc/autorandr.nix
index 072064143db..aa96acb6130 100644
--- a/nixos/modules/services/misc/autorandr.nix
+++ b/nixos/modules/services/misc/autorandr.nix
@@ -254,6 +254,12 @@ in {
         '';
       };
 
+      ignoreLid = mkOption {
+        default = false;
+        type = types.bool;
+        description = lib.mdDoc "Treat outputs as connected even if their lids are closed";
+      };
+
       hooks = mkOption {
         type = hooksModule;
         description = lib.mdDoc "Global hook scripts";
@@ -340,7 +346,13 @@ in {
       startLimitIntervalSec = 5;
       startLimitBurst = 1;
       serviceConfig = {
-        ExecStart = "${pkgs.autorandr}/bin/autorandr --batch --change --default ${cfg.defaultTarget}";
+        ExecStart = ''
+          ${pkgs.autorandr}/bin/autorandr \
+            --batch \
+            --change \
+            --default ${cfg.defaultTarget} \
+            ${optionalString cfg.ignoreLid "--ignore-lid"}
+        '';
         Type = "oneshot";
         RemainAfterExit = false;
         KillMode = "process";
diff --git a/nixos/modules/services/misc/autosuspend.nix b/nixos/modules/services/misc/autosuspend.nix
new file mode 100644
index 00000000000..b3e362533a0
--- /dev/null
+++ b/nixos/modules/services/misc/autosuspend.nix
@@ -0,0 +1,230 @@
+{ config, pkgs, lib, ... }:
+let
+  inherit (lib) mapAttrs' nameValuePair filterAttrs types mkEnableOption
+    mdDoc mkPackageOptionMD mkOption literalExpression mkIf flatten
+    maintainers attrValues;
+
+  cfg = config.services.autosuspend;
+
+  settingsFormat = pkgs.formats.ini { };
+
+  checks =
+    mapAttrs'
+      (n: v: nameValuePair "check.${n}" (filterAttrs (_: v: v != null) v))
+      cfg.checks;
+  wakeups =
+    mapAttrs'
+      (n: v: nameValuePair "wakeup.${n}" (filterAttrs (_: v: v != null) v))
+      cfg.wakeups;
+
+  # Whether the given check is enabled
+  hasCheck = class:
+    (filterAttrs
+      (n: v: v.enabled && (if v.class == null then n else v.class) == class)
+      cfg.checks)
+    != { };
+
+  # Dependencies needed by specific checks
+  dependenciesForChecks = {
+    "Smb" = pkgs.samba;
+    "XIdleTime" = [ pkgs.xprintidle pkgs.sudo ];
+  };
+
+  autosuspend-conf =
+    settingsFormat.generate "autosuspend.conf" ({ general = cfg.settings; } // checks // wakeups);
+
+  autosuspend = cfg.package;
+
+  checkType = types.submodule {
+    freeformType = settingsFormat.type.nestedTypes.elemType;
+
+    options.enabled = mkEnableOption (mdDoc "this activity check") // { default = true; };
+
+    options.class = mkOption {
+      default = null;
+      type = with types; nullOr (enum [
+        "ActiveCalendarEvent"
+        "ActiveConnection"
+        "ExternalCommand"
+        "JsonPath"
+        "Kodi"
+        "KodiIdleTime"
+        "LastLogActivity"
+        "Load"
+        "LogindSessionsIdle"
+        "Mpd"
+        "NetworkBandwidth"
+        "Ping"
+        "Processes"
+        "Smb"
+        "Users"
+        "XIdleTime"
+        "XPath"
+      ]);
+      description = mdDoc ''
+        Name of the class implementing the check.  If this option is not specified, the check's
+        name must represent a valid internal check class.
+      '';
+    };
+  };
+
+  wakeupType = types.submodule {
+    freeformType = settingsFormat.type.nestedTypes.elemType;
+
+    options.enabled = mkEnableOption (mdDoc "this wake-up check") // { default = true; };
+
+    options.class = mkOption {
+      default = null;
+      type = with types; nullOr (enum [
+        "Calendar"
+        "Command"
+        "File"
+        "Periodic"
+        "SystemdTimer"
+        "XPath"
+        "XPathDelta"
+      ]);
+      description = mdDoc ''
+        Name of the class implementing the check.  If this option is not specified, the check's
+        name must represent a valid internal check class.
+      '';
+    };
+  };
+in
+{
+  options = {
+    services.autosuspend = {
+      enable = mkEnableOption (mdDoc "the autosuspend daemon");
+
+      package = mkPackageOptionMD pkgs "autosuspend" { };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType = settingsFormat.type.nestedTypes.elemType;
+
+          options = {
+            # Provide reasonable defaults for these two (required) options
+            suspend_cmd = mkOption {
+              default = "systemctl suspend";
+              type = with types; str;
+              description = mdDoc ''
+                The command to execute in case the host shall be suspended. This line can contain
+                additional command line arguments to the command to execute.
+              '';
+            };
+            wakeup_cmd = mkOption {
+              default = ''sh -c 'echo 0 > /sys/class/rtc/rtc0/wakealarm && echo {timestamp:.0f} > /sys/class/rtc/rtc0/wakealarm' '';
+              type = with types; str;
+              description = mdDoc ''
+                The command to execute for scheduling a wake up of the system. The given string is
+                processed using Python’s `str.format()` and a format argument called `timestamp`
+                encodes the UTC timestamp of the planned wake up time (float). Additionally `iso`
+                can be used to acquire the timestamp in ISO 8601 format.
+              '';
+            };
+          };
+        };
+        default = { };
+        example = literalExpression ''
+          {
+            enable = true;
+            interval = 30;
+            idle_time = 120;
+          }
+        '';
+        description = mdDoc ''
+          Configuration for autosuspend, see
+          <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#general-configuration>
+          for supported values.
+        '';
+      };
+
+      checks = mkOption {
+        default = { };
+        type = with types; attrsOf checkType;
+        description = mdDoc ''
+          Checks for activity.  For more information, see:
+           - <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#activity-check-configuration>
+           - <https://autosuspend.readthedocs.io/en/latest/available_checks.html>
+        '';
+        example = literalExpression ''
+          {
+            # Basic activity check configuration.
+            # The check class name is derived from the section header (Ping in this case).
+            # Remember to enable desired checks. They are disabled by default.
+            Ping = {
+              hosts = "192.168.0.7";
+            };
+
+            # This check is disabled.
+            Smb.enabled = false;
+
+            # Example for a custom check name.
+            # This will use the Users check with the custom name RemoteUsers.
+            # Custom names are necessary in case a check class is used multiple times.
+            # Custom names can also be used for clarification.
+            RemoteUsers = {
+              class = "Users";
+              name = ".*";
+              terminal = ".*";
+              host = "[0-9].*";
+            };
+
+            # Here the Users activity check is used again with different settings and a different name
+            LocalUsers = {
+              class = "Users";
+              name = ".*";
+              terminal = ".*";
+              host = "localhost";
+            };
+          }
+        '';
+      };
+
+      wakeups = mkOption {
+        default = { };
+        type = with types; attrsOf wakeupType;
+        description = mdDoc ''
+          Checks for wake up.  For more information, see:
+           - <https://autosuspend.readthedocs.io/en/latest/configuration_file.html#wake-up-check-configuration>
+           - <https://autosuspend.readthedocs.io/en/latest/available_wakeups.html>
+        '';
+        example = literalExpression ''
+          {
+            # Wake up checks reuse the same configuration mechanism as activity checks.
+            Calendar = {
+              url = "http://example.org/test.ics";
+            };
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.autosuspend = {
+      description = "A daemon to suspend your server in case of inactivity";
+      documentation = [ "https://autosuspend.readthedocs.io/en/latest/systemd_integration.html" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = flatten (attrValues (filterAttrs (n: _: hasCheck n) dependenciesForChecks));
+      serviceConfig = {
+        ExecStart = ''${autosuspend}/bin/autosuspend -l ${autosuspend}/etc/autosuspend-logging.conf -c ${autosuspend-conf} daemon'';
+      };
+    };
+
+    systemd.services.autosuspend-detect-suspend = {
+      description = "Notifies autosuspend about suspension";
+      documentation = [ "https://autosuspend.readthedocs.io/en/latest/systemd_integration.html" ];
+      wantedBy = [ "sleep.target" ];
+      after = [ "sleep.target" ];
+      serviceConfig = {
+        ExecStart = ''${autosuspend}/bin/autosuspend -l ${autosuspend}/etc/autosuspend-logging.conf -c ${autosuspend-conf} presuspend'';
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with maintainers; [ xlambein ];
+  };
+}
diff --git a/nixos/modules/services/misc/etebase-server.nix b/nixos/modules/services/misc/etebase-server.nix
index c3723d18814..045048a1a2e 100644
--- a/nixos/modules/services/misc/etebase-server.nix
+++ b/nixos/modules/services/misc/etebase-server.nix
@@ -179,21 +179,21 @@ in
       description = "An Etebase (EteSync 2.0) server";
       after = [ "network.target" "systemd-tmpfiles-setup.service" ];
       wantedBy = [ "multi-user.target" ];
+      path = [ pythonEnv ];
       serviceConfig = {
         User = cfg.user;
         Restart = "always";
         WorkingDirectory = cfg.dataDir;
       };
       environment = {
-        PYTHONPATH = "${pythonEnv}/${pkgs.python3.sitePackages}";
         ETEBASE_EASY_CONFIG_PATH = configIni;
       };
       preStart = ''
         # Auto-migrate on first run or if the package has changed
         versionFile="${cfg.dataDir}/src-version"
         if [[ $(cat "$versionFile" 2>/dev/null) != ${pkgs.etebase-server} ]]; then
-          ${pythonEnv}/bin/etebase-server migrate --no-input
-          ${pythonEnv}/bin/etebase-server collectstatic --no-input --clear
+          etebase-server migrate --no-input
+          etebase-server collectstatic --no-input --clear
           echo ${pkgs.etebase-server} > "$versionFile"
         fi
       '';
@@ -204,7 +204,7 @@ in
           else "-b 0.0.0.0 -p ${toString cfg.port}";
         in ''
           cd "${pythonEnv}/lib/etebase-server";
-          ${pythonEnv}/bin/daphne ${networking} \
+          daphne ${networking} \
             etebase_server.asgi:application
         '';
     };
diff --git a/nixos/modules/services/misc/fstrim.nix b/nixos/modules/services/misc/fstrim.nix
index 36b5f9c8cca..55fb24e2927 100644
--- a/nixos/modules/services/misc/fstrim.nix
+++ b/nixos/modules/services/misc/fstrim.nix
@@ -34,7 +34,7 @@ in {
 
     systemd.timers.fstrim = {
       timerConfig = {
-        OnCalendar = cfg.interval;
+        OnCalendar = [ "" cfg.interval ];
       };
       wantedBy = [ "timers.target" ];
     };
diff --git a/nixos/modules/services/misc/gammu-smsd.nix b/nixos/modules/services/misc/gammu-smsd.nix
index 83f4efe695a..eff725f5a86 100644
--- a/nixos/modules/services/misc/gammu-smsd.nix
+++ b/nixos/modules/services/misc/gammu-smsd.nix
@@ -10,7 +10,7 @@ let
     Connection = ${cfg.device.connection}
     SynchronizeTime = ${if cfg.device.synchronizeTime then "yes" else "no"}
     LogFormat = ${cfg.log.format}
-    ${if (cfg.device.pin != null) then "PIN = ${cfg.device.pin}" else ""}
+    ${optionalString (cfg.device.pin != null) "PIN = ${cfg.device.pin}"}
     ${cfg.extraConfig.gammu}
 
 
@@ -33,10 +33,10 @@ let
     ${optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "native_pgsql") (
       with cfg.backend; ''
         Driver = ${sql.driver}
-        ${if (sql.database!= null) then "Database = ${sql.database}" else ""}
-        ${if (sql.host != null) then "Host = ${sql.host}" else ""}
-        ${if (sql.user != null) then "User = ${sql.user}" else ""}
-        ${if (sql.password != null) then "Password = ${sql.password}" else ""}
+        ${optionalString (sql.database!= null) "Database = ${sql.database}"}
+        ${optionalString (sql.host != null) "Host = ${sql.host}"}
+        ${optionalString (sql.user != null) "User = ${sql.user}"}
+        ${optionalString (sql.password != null) "Password = ${sql.password}"}
       '')}
 
     ${cfg.extraConfig.smsd}
diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix
index ceb4c117285..0c414c2466b 100644
--- a/nixos/modules/services/misc/gitea.nix
+++ b/nixos/modules/services/misc/gitea.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.gitea;
   opt = options.services.gitea;
-  gitea = cfg.package;
+  exe = lib.getExe cfg.package;
   pg = config.services.postgresql;
   useMysql = cfg.database.type == "mysql";
   usePostgresql = cfg.database.type == "postgres";
@@ -26,9 +26,18 @@ in
   imports = [
     (mkRenamedOptionModule [ "services" "gitea" "cookieSecure" ] [ "services" "gitea" "settings" "session" "COOKIE_SECURE" ])
     (mkRenamedOptionModule [ "services" "gitea" "disableRegistration" ] [ "services" "gitea" "settings" "service" "DISABLE_REGISTRATION" ])
+    (mkRenamedOptionModule [ "services" "gitea" "domain" ] [ "services" "gitea" "settings" "server" "DOMAIN" ])
+    (mkRenamedOptionModule [ "services" "gitea" "httpAddress" ] [ "services" "gitea" "settings" "server" "HTTP_ADDR" ])
+    (mkRenamedOptionModule [ "services" "gitea" "httpPort" ] [ "services" "gitea" "settings" "server" "HTTP_PORT" ])
     (mkRenamedOptionModule [ "services" "gitea" "log" "level" ] [ "services" "gitea" "settings" "log" "LEVEL" ])
     (mkRenamedOptionModule [ "services" "gitea" "log" "rootPath" ] [ "services" "gitea" "settings" "log" "ROOT_PATH" ])
+    (mkRenamedOptionModule [ "services" "gitea" "rootUrl" ] [ "services" "gitea" "settings" "server" "ROOT_URL" ])
     (mkRenamedOptionModule [ "services" "gitea" "ssh" "clonePort" ] [ "services" "gitea" "settings" "server" "SSH_PORT" ])
+    (mkRenamedOptionModule [ "services" "gitea" "staticRootPath" ] [ "services" "gitea" "settings" "server" "STATIC_ROOT_PATH" ])
+
+    (mkChangedOptionModule [ "services" "gitea" "enableUnixSocket" ] [ "services" "gitea" "settings" "server" "PROTOCOL" ] (
+      config: if config.services.gitea.enableUnixSocket then "http+unix" else "http"
+    ))
 
     (mkRemovedOptionModule [ "services" "gitea" "ssh" "enable" ] "services.gitea.ssh.enable has been migrated into freeform setting services.gitea.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted")
   ];
@@ -57,7 +66,14 @@ in
       stateDir = mkOption {
         default = "/var/lib/gitea";
         type = types.str;
-        description = lib.mdDoc "gitea data directory.";
+        description = lib.mdDoc "Gitea data directory.";
+      };
+
+      customDir = mkOption {
+        default = "${cfg.stateDir}/custom";
+        defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"'';
+        type = types.str;
+        description = lib.mdDoc "Gitea custom directory. Used for config, custom templates and other options.";
       };
 
       user = mkOption {
@@ -66,6 +82,12 @@ in
         description = lib.mdDoc "User account under which gitea runs.";
       };
 
+      group = mkOption {
+        type = types.str;
+        default = "gitea";
+        description = lib.mdDoc "Group under which gitea runs.";
+      };
+
       database = {
         type = mkOption {
           type = types.enum [ "sqlite3" "mysql" "postgres" ];
@@ -175,7 +197,7 @@ in
         };
 
         type = mkOption {
-          type = types.enum [ "zip" "rar" "tar" "sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" ];
+          type = types.enum [ "zip" "rar" "tar" "sz" "tar.gz" "tar.xz" "tar.bz2" "tar.br" "tar.lz4" "tar.zst" ];
           default = "zip";
           description = lib.mdDoc "Archive format used to store the dump file.";
         };
@@ -216,44 +238,6 @@ in
         description = lib.mdDoc "Path to the git repositories.";
       };
 
-      domain = mkOption {
-        type = types.str;
-        default = "localhost";
-        description = lib.mdDoc "Domain name of your server.";
-      };
-
-      rootUrl = mkOption {
-        type = types.str;
-        default = "http://localhost:3000/";
-        description = lib.mdDoc "Full public URL of gitea server.";
-      };
-
-      httpAddress = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
-        description = lib.mdDoc "HTTP listen address.";
-      };
-
-      httpPort = mkOption {
-        type = types.port;
-        default = 3000;
-        description = lib.mdDoc "HTTP listen port.";
-      };
-
-      enableUnixSocket = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Configure Gitea to listen on a unix socket instead of the default TCP port.";
-      };
-
-      staticRootPath = mkOption {
-        type = types.either types.str types.path;
-        default = gitea.data;
-        defaultText = literalExpression "package.data";
-        example = "/var/lib/gitea/data";
-        description = lib.mdDoc "Upper level of template and static files path.";
-      };
-
       mailerPasswordFile = mkOption {
         type = types.nullOr types.str;
         default = null;
@@ -285,7 +269,7 @@ in
             };
           }
         '';
-        type = with types; submodule {
+        type = types.submodule {
           freeformType = format.type;
           options = {
             log = {
@@ -303,6 +287,46 @@ in
             };
 
             server = {
+              PROTOCOL = mkOption {
+                type = types.enum [ "http" "https" "fcgi" "http+unix" "fcgi+unix" ];
+                default = "http";
+                description = lib.mdDoc ''Listen protocol. `+unix` means "over unix", not "in addition to."'';
+              };
+
+              HTTP_ADDR = mkOption {
+                type = types.either types.str types.path;
+                default = if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0";
+                defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0"'';
+                description = lib.mdDoc "Listen address. Must be a path when using a unix socket.";
+              };
+
+              HTTP_PORT = mkOption {
+                type = types.port;
+                default = 3000;
+                description = lib.mdDoc "Listen port. Ignored when using a unix socket.";
+              };
+
+              DOMAIN = mkOption {
+                type = types.str;
+                default = "localhost";
+                description = lib.mdDoc "Domain name of your server.";
+              };
+
+              ROOT_URL = mkOption {
+                type = types.str;
+                default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/";
+                defaultText = literalExpression ''"http://''${config.services.gitea.settings.server.DOMAIN}:''${toString config.services.gitea.settings.server.HTTP_PORT}/"'';
+                description = lib.mdDoc "Full public URL of gitea server.";
+              };
+
+              STATIC_ROOT_PATH = mkOption {
+                type = types.either types.str types.path;
+                default = cfg.package.data;
+                defaultText = literalExpression "config.${opt.package}.data";
+                example = "/var/lib/gitea/data";
+                description = lib.mdDoc "Upper level of template and static files path.";
+              };
+
               DISABLE_SSH = mkOption {
                 type = types.bool;
                 default = false;
@@ -359,12 +383,14 @@ in
 
   config = mkIf cfg.enable {
     assertions = [
-      { assertion = cfg.database.createDatabase -> cfg.database.user == cfg.user;
+      { assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user;
         message = "services.gitea.database.user must match services.gitea.user if the database is to be automatically provisioned";
       }
     ];
 
     services.gitea.settings = {
+      "cron.update_checker".ENABLED = lib.mkDefault false;
+
       database = mkMerge [
         {
           DB_TYPE = cfg.database.type;
@@ -387,27 +413,10 @@ in
         ROOT = cfg.repositoryRoot;
       };
 
-      server = mkMerge [
-        {
-          DOMAIN = cfg.domain;
-          STATIC_ROOT_PATH = toString cfg.staticRootPath;
-          LFS_JWT_SECRET = "#lfsjwtsecret#";
-          ROOT_URL = cfg.rootUrl;
-        }
-        (mkIf cfg.enableUnixSocket {
-          PROTOCOL = "unix";
-          HTTP_ADDR = "/run/gitea/gitea.sock";
-        })
-        (mkIf (!cfg.enableUnixSocket) {
-          HTTP_ADDR = cfg.httpAddress;
-          HTTP_PORT = cfg.httpPort;
-        })
-        (mkIf cfg.lfs.enable {
-          LFS_START_SERVER = true;
-          LFS_CONTENT_PATH = cfg.lfs.contentDir;
-        })
-
-      ];
+      server = mkIf cfg.lfs.enable {
+        LFS_START_SERVER = true;
+        LFS_JWT_SECRET = "#lfsjwtsecret#";
+      };
 
       session = {
         COOKIE_NAME = lib.mkDefault "session";
@@ -426,6 +435,10 @@ in
       oauth2 = {
         JWT_SECRET = "#oauth2jwtsecret#";
       };
+
+      lfs = mkIf cfg.lfs.enable {
+        PATH = cfg.lfs.contentDir;
+      };
     };
 
     services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) {
@@ -452,38 +465,42 @@ in
     };
 
     systemd.tmpfiles.rules = [
-      "d '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.dump.backupDir}' - ${cfg.user} gitea - -"
-      "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.lfs.contentDir}' - ${cfg.user} gitea - -"
-      "d '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.repositoryRoot}' - ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
-      "d '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -"
-      "z '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -"
-      "Z '${cfg.stateDir}' - ${cfg.user} gitea - -"
+      "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "Z '${cfg.dump.backupDir}' - ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -"
+      "Z '${cfg.repositoryRoot}' - ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
+      "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -"
+      "Z '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
 
       # If we have a folder or symlink with gitea locales, remove it
       # And symlink the current gitea locales in place
-      "L+ '${cfg.stateDir}/conf/locale' - - - - ${gitea.out}/locale"
+      "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale"
+
+    ] ++ lib.optionals cfg.lfs.enable [
+      "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -"
+      "Z '${cfg.lfs.contentDir}' - ${cfg.user} ${cfg.group} - -"
     ];
 
     systemd.services.gitea = {
       description = "gitea";
       after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service";
       wantedBy = [ "multi-user.target" ];
-      path = [ gitea pkgs.git pkgs.gnupg ];
+      path = [ cfg.package pkgs.git pkgs.gnupg ];
 
       # In older versions the secret naming for JWT was kind of confusing.
       # The file jwt_secret hold the value for LFS_JWT_SECRET and JWT_SECRET
@@ -493,47 +510,52 @@ in
       # lfs_jwt_secret.
       # We have to consider this to stay compatible with older installations.
       preStart = let
-        runConfig = "${cfg.stateDir}/custom/conf/app.ini";
-        secretKey = "${cfg.stateDir}/custom/conf/secret_key";
-        oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret";
-        oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET
-        lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
-        internalToken = "${cfg.stateDir}/custom/conf/internal_token";
+        runConfig = "${cfg.customDir}/conf/app.ini";
+        secretKey = "${cfg.customDir}/conf/secret_key";
+        oauth2JwtSecret = "${cfg.customDir}/conf/oauth2_jwt_secret";
+        oldLfsJwtSecret = "${cfg.customDir}/conf/jwt_secret"; # old file for LFS_JWT_SECRET
+        lfsJwtSecret = "${cfg.customDir}/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET
+        internalToken = "${cfg.customDir}/conf/internal_token";
         replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret";
       in ''
-        # copy custom configuration and generate a random secret key if needed
+        # copy custom configuration and generate random secrets if needed
         ${optionalString (!cfg.useWizard) ''
           function gitea_setup {
-            cp -f ${configFile} ${runConfig}
+            cp -f '${configFile}' '${runConfig}'
 
-            if [ ! -s ${secretKey} ]; then
-                ${gitea}/bin/gitea generate secret SECRET_KEY > ${secretKey}
+            if [ ! -s '${secretKey}' ]; then
+                ${exe} generate secret SECRET_KEY > '${secretKey}'
             fi
 
             # Migrate LFS_JWT_SECRET filename
-            if [[ -s ${oldLfsJwtSecret} && ! -s ${lfsJwtSecret} ]]; then
-                mv ${oldLfsJwtSecret} ${lfsJwtSecret}
+            if [[ -s '${oldLfsJwtSecret}' && ! -s '${lfsJwtSecret}' ]]; then
+                mv '${oldLfsJwtSecret}' '${lfsJwtSecret}'
             fi
 
-            if [ ! -s ${oauth2JwtSecret} ]; then
-                ${gitea}/bin/gitea generate secret JWT_SECRET > ${oauth2JwtSecret}
+            if [ ! -s '${oauth2JwtSecret}' ]; then
+                ${exe} generate secret JWT_SECRET > '${oauth2JwtSecret}'
             fi
 
-            if [ ! -s ${lfsJwtSecret} ]; then
-                ${gitea}/bin/gitea generate secret LFS_JWT_SECRET > ${lfsJwtSecret}
+            ${lib.optionalString cfg.lfs.enable ''
+            if [ ! -s '${lfsJwtSecret}' ]; then
+                ${exe} generate secret LFS_JWT_SECRET > '${lfsJwtSecret}'
             fi
+            ''}
 
-            if [ ! -s ${internalToken} ]; then
-                ${gitea}/bin/gitea generate secret INTERNAL_TOKEN > ${internalToken}
+            if [ ! -s '${internalToken}' ]; then
+                ${exe} generate secret INTERNAL_TOKEN > '${internalToken}'
             fi
 
             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.lfs.enable ''
+              ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}'
+            ''}
+
             ${lib.optionalString (cfg.mailerPasswordFile != null) ''
               ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}'
             ''}
@@ -543,30 +565,30 @@ in
         ''}
 
         # run migrations/init the database
-        ${gitea}/bin/gitea migrate
+        ${exe} migrate
 
         # update all hooks' binary paths
-        ${gitea}/bin/gitea admin regenerate hooks
+        ${exe} admin regenerate hooks
 
         # update command option in authorized_keys
         if [ -r ${cfg.stateDir}/.ssh/authorized_keys ]
         then
-          ${gitea}/bin/gitea admin regenerate keys
+          ${exe} admin regenerate keys
         fi
       '';
 
       serviceConfig = {
         Type = "simple";
         User = cfg.user;
-        Group = "gitea";
+        Group = cfg.group;
         WorkingDirectory = cfg.stateDir;
-        ExecStart = "${gitea}/bin/gitea web --pid /run/gitea/gitea.pid";
+        ExecStart = "${exe} web --pid /run/gitea/gitea.pid";
         Restart = "always";
         # Runtime directory and mode
         RuntimeDirectory = "gitea";
         RuntimeDirectoryMode = "0755";
         # Access write directories
-        ReadWritePaths = [ cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ];
+        ReadWritePaths = [ cfg.customDir cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ];
         UMask = "0027";
         # Capabilities
         CapabilityBoundingSet = "";
@@ -592,13 +614,14 @@ in
         PrivateMounts = true;
         # System Call Filtering
         SystemCallArchitectures = "native";
-        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @memlock @module @mount @obsolete @raw-io @reboot @setuid @swap";
+        SystemCallFilter = "~@clock @cpu-emulation @debug @keyring @module @mount @obsolete @raw-io @reboot @setuid @swap";
       };
 
       environment = {
         USER = cfg.user;
         HOME = cfg.stateDir;
         GITEA_WORK_DIR = cfg.stateDir;
+        GITEA_CUSTOM = cfg.customDir;
       };
     };
 
@@ -607,12 +630,14 @@ in
         description = "Gitea Service";
         home = cfg.stateDir;
         useDefaultShell = true;
-        group = "gitea";
+        group = cfg.group;
         isSystemUser = true;
       };
     };
 
-    users.groups.gitea = {};
+    users.groups = mkIf (cfg.group == "gitea") {
+      gitea = {};
+    };
 
     warnings =
       optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++
@@ -630,8 +655,7 @@ in
     systemd.services.gitea-dump = mkIf cfg.dump.enable {
        description = "gitea dump";
        after = [ "gitea.service" ];
-       wantedBy = [ "default.target" ];
-       path = [ gitea ];
+       path = [ cfg.package ];
 
        environment = {
          USER = cfg.user;
@@ -642,7 +666,7 @@ in
        serviceConfig = {
          Type = "oneshot";
          User = cfg.user;
-         ExecStart = "${gitea}/bin/gitea dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
+         ExecStart = "${exe} dump --type ${cfg.dump.type}" + optionalString (cfg.dump.file != null) " --file ${cfg.dump.file}";
          WorkingDirectory = cfg.dump.backupDir;
        };
     };
@@ -654,5 +678,5 @@ in
       timerConfig.OnCalendar = cfg.dump.interval;
     };
   };
-  meta.maintainers = with lib.maintainers; [ srhb ma27 ];
+  meta.maintainers = with lib.maintainers; [ srhb ma27 thehedgeh0g ];
 }
diff --git a/nixos/modules/services/misc/gitit.nix b/nixos/modules/services/misc/gitit.nix
deleted file mode 100644
index 0fafa76b548..00000000000
--- a/nixos/modules/services/misc/gitit.nix
+++ /dev/null
@@ -1,725 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.gitit;
-
-  homeDir = "/var/lib/gitit";
-
-  toYesNo = b: if b then "yes" else "no";
-
-  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));
-
-  gititSh = hsPkgs: extras: with pkgs; let
-    env = gititWithPkgs hsPkgs extras;
-  in writeScript "gitit" ''
-    #!${runtimeShell}
-    cd $HOME
-    export NIX_GHC="${env}/bin/ghc"
-    export NIX_GHCPKG="${env}/bin/ghc-pkg"
-    export NIX_GHC_DOCDIR="${env}/share/doc/ghc/html"
-    export NIX_GHC_LIBDIR=$( $NIX_GHC --print-libdir )
-    ${env}/bin/gitit -f ${configFile}
-  '';
-
-  gititOptions = {
-
-      enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Enable the gitit service.";
-      };
-
-      haskellPackages = mkOption {
-        default = pkgs.haskellPackages;
-        defaultText = literalExpression "pkgs.haskellPackages";
-        example = literalExpression "pkgs.haskell.packages.ghc784";
-        description = lib.mdDoc "haskellPackages used to build gitit and plugins.";
-      };
-
-      extraPackages = mkOption {
-        type = types.functionTo (types.listOf types.package);
-        default = self: [];
-        example = literalExpression ''
-          haskellPackages: [
-            haskellPackages.wreq
-          ]
-        '';
-        description = lib.mdDoc ''
-          Extra packages available to ghc when running gitit. The
-          value must be a function which receives the attrset defined
-          in {var}`haskellPackages` as the sole argument.
-        '';
-      };
-
-      address = mkOption {
-        type = types.str;
-        default = "0.0.0.0";
-        description = lib.mdDoc "IP address on which the web server will listen.";
-      };
-
-      port = mkOption {
-        type = types.int;
-        default = 5001;
-        description = lib.mdDoc "Port on which the web server will run.";
-      };
-
-      wikiTitle = mkOption {
-        type = types.str;
-        default = "Gitit!";
-        description = lib.mdDoc "The wiki title.";
-      };
-
-      repositoryType = mkOption {
-        type = types.enum ["git" "darcs" "mercurial"];
-        default = "git";
-        description = lib.mdDoc "Specifies the type of repository used for wiki content.";
-      };
-
-      repositoryPath = mkOption {
-        type = types.path;
-        default = homeDir + "/wiki";
-        description = lib.mdDoc ''
-          Specifies the path of the repository directory. If it does not
-          exist, gitit will create it on startup.
-        '';
-      };
-
-      requireAuthentication = mkOption {
-        type = types.enum [ "none" "modify" "read" ];
-        default = "modify";
-        description = lib.mdDoc ''
-          If 'none', login is never required, and pages can be edited
-          anonymously.  If 'modify', login is required to modify the wiki
-          (edit, add, delete pages, upload files).  If 'read', login is
-          required to see any wiki pages.
-        '';
-      };
-
-      authenticationMethod = mkOption {
-        type = types.enum [ "form" "http" "generic" "github" ];
-        default = "form";
-        description = lib.mdDoc ''
-          'form' means that users will be logged in and registered using forms
-          in the gitit web interface.  'http' means that gitit will assume that
-          HTTP authentication is in place and take the logged in username from
-          the "Authorization" field of the HTTP request header (in addition,
-          the login/logout and registration links will be suppressed).
-          'generic' means that gitit will assume that some form of
-          authentication is in place that directly sets REMOTE_USER to the name
-          of the authenticated user (e.g. mod_auth_cas on apache).  'rpx' means
-          that gitit will attempt to log in through https://rpxnow.com.  This
-          requires that 'rpx-domain', 'rpx-key', and 'base-url' be set below,
-          and that 'curl' be in the system path.
-        '';
-      };
-
-      userFile = mkOption {
-        type = types.path;
-        default = homeDir + "/gitit-users";
-        description = lib.mdDoc ''
-          Specifies the path of the file containing user login information.  If
-          it does not exist, gitit will create it (with an empty user list).
-          This file is not used if 'http' is selected for
-          authentication-method.
-        '';
-      };
-
-      sessionTimeout = mkOption {
-        type = types.int;
-        default = 60;
-        description = lib.mdDoc ''
-          Number of minutes of inactivity before a session expires.
-        '';
-      };
-
-      staticDir = mkOption {
-        type = types.path;
-        default = gititShared + "/data/static";
-        description = lib.mdDoc ''
-          Specifies the path of the static directory (containing javascript,
-          css, and images).  If it does not exist, gitit will create it and
-          populate it with required scripts, stylesheets, and images.
-        '';
-      };
-
-      defaultPageType = mkOption {
-        type = types.enum [ "markdown" "rst" "latex" "html" "markdown+lhs" "rst+lhs" "latex+lhs" ];
-        default = "markdown";
-        description = lib.mdDoc ''
-          Specifies the type of markup used to interpret pages in the wiki.
-          Possible values are markdown, rst, latex, html, markdown+lhs,
-          rst+lhs, and latex+lhs. (the +lhs variants treat the input as
-          literate Haskell. See pandoc's documentation for more details.) If
-          Markdown is selected, pandoc's syntax extensions (for footnotes,
-          delimited code blocks, etc.) will be enabled. Note that pandoc's
-          restructuredtext parser is not complete, so some pages may not be
-          rendered correctly if rst is selected. The same goes for latex and
-          html.
-        '';
-      };
-
-      math = mkOption {
-        type = types.enum [ "mathml" "raw" "mathjax" "jsmath" "google" ];
-        default = "mathml";
-        description = lib.mdDoc ''
-          Specifies how LaTeX math is to be displayed.  Possible values are
-          mathml, raw, mathjax, jsmath, and google.  If mathml is selected,
-          gitit will convert LaTeX math to MathML and link in a script,
-          MathMLinHTML.js, that allows the MathML to be seen in Gecko browsers,
-          IE + mathplayer, and Opera. In other browsers you may get a jumble of
-          characters.  If raw is selected, the LaTeX math will be displayed as
-          raw LaTeX math.  If mathjax is selected, gitit will link to the
-          remote mathjax script.  If jsMath is selected, gitit will link to the
-          script /js/jsMath/easy/load.js, and will assume that jsMath has been
-          installed into the js/jsMath directory.  This is the most portable
-          solution. If google is selected, the google chart API is called to
-          render the formula as an image. This requires a connection to google,
-          and might raise a technical or a privacy problem.
-        '';
-      };
-
-      mathJaxScript = mkOption {
-        type = types.str;
-        default = "https://d3eoax9i5htok0.cloudfront.net/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML";
-        description = lib.mdDoc ''
-          Specifies the path to MathJax rendering script.  You might want to
-          use your own MathJax script to render formulas without Internet
-          connection or if you want to use some special LaTeX packages.  Note:
-          path specified there cannot be an absolute path to a script on your
-          hdd, instead you should run your (local if you wish) HTTP server
-          which will serve the MathJax.js script. You can easily (in four lines
-          of code) serve MathJax.js using
-          http://happstack.com/docs/crashcourse/FileServing.html Do not forget
-          the "http://" prefix (e.g. http://localhost:1234/MathJax.js).
-        '';
-      };
-
-      showLhsBirdTracks = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether to show Haskell code blocks in "bird style", with
-          "> " at the beginning of each line.
-        '';
-      };
-
-      templatesDir = mkOption {
-        type = types.path;
-        default = gititShared + "/data/templates";
-        description = lib.mdDoc ''
-          Specifies the path of the directory containing page templates.  If it
-          does not exist, gitit will create it with default templates.  Users
-          may wish to edit the templates to customize the appearance of their
-          wiki. The template files are HStringTemplate templates.  Variables to
-          be interpolated appear between $\'s. Literal $\'s must be
-          backslash-escaped.
-        '';
-      };
-
-      logFile = mkOption {
-        type = types.path;
-        default = homeDir + "/gitit.log";
-        description = lib.mdDoc ''
-          Specifies the path of gitit's log file.  If it does not exist, gitit
-          will create it. The log is in Apache combined log format.
-        '';
-      };
-
-      logLevel = mkOption {
-        type = types.enum [ "DEBUG" "INFO" "NOTICE" "WARNING" "ERROR" "CRITICAL" "ALERT" "EMERGENCY" ];
-        default = "ERROR";
-        description = lib.mdDoc ''
-          Determines how much information is logged.  Possible values (from
-          most to least verbose) are DEBUG, INFO, NOTICE, WARNING, ERROR,
-          CRITICAL, ALERT, EMERGENCY.
-        '';
-      };
-
-      frontPage = mkOption {
-        type = types.str;
-        default = "Front Page";
-        description = lib.mdDoc ''
-          Specifies which wiki page is to be used as the wiki's front page.
-          Gitit creates a default front page on startup, if one does not exist
-          already.
-        '';
-      };
-
-      noDelete = mkOption {
-        type = types.str;
-        default = "Front Page, Help";
-        description = lib.mdDoc ''
-          Specifies pages that cannot be deleted through the web interface.
-          (They can still be deleted directly using git or darcs.) A
-          comma-separated list of page names.  Leave blank to allow every page
-          to be deleted.
-        '';
-      };
-
-      noEdit = mkOption {
-        type = types.str;
-        default = "Help";
-        description = lib.mdDoc ''
-          Specifies pages that cannot be edited through the web interface.
-          Leave blank to allow every page to be edited.
-        '';
-      };
-
-      defaultSummary = mkOption {
-        type = types.str;
-        default = "";
-        description = lib.mdDoc ''
-          Specifies text to be used in the change description if the author
-          leaves the "description" field blank.  If default-summary is blank
-          (the default), the author will be required to fill in the description
-          field.
-        '';
-      };
-
-      tableOfContents = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Specifies whether to print a tables of contents (with links to
-          sections) on each wiki page.
-        '';
-      };
-
-      plugins = mkOption {
-        type = with types; listOf str;
-        default = [ (gititShared + "/plugins/Dot.hs") ];
-        description = lib.mdDoc ''
-          Specifies a list of plugins to load. Plugins may be specified either
-          by their path or by their module name. If the plugin name starts
-          with Gitit.Plugin., gitit will assume that the plugin is an installed
-          module and will not try to find a source file.
-        '';
-      };
-
-      useCache = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether to cache rendered pages.  Note that if use-feed is
-          selected, feeds will be cached regardless of the value of use-cache.
-        '';
-      };
-
-      cacheDir = mkOption {
-        type = types.path;
-        default = homeDir + "/cache";
-        description = lib.mdDoc "Path where rendered pages will be cached.";
-      };
-
-      maxUploadSize = mkOption {
-        type = types.str;
-        default = "1000K";
-        description = lib.mdDoc ''
-          Specifies an upper limit on the size (in bytes) of files uploaded
-          through the wiki's web interface.  To disable uploads, set this to
-          0K.  This will result in the uploads link disappearing and the
-          _upload url becoming inactive.
-        '';
-      };
-
-      maxPageSize = mkOption {
-        type = types.str;
-        default = "1000K";
-        description = lib.mdDoc "Specifies an upper limit on the size (in bytes) of pages.";
-      };
-
-      debugMode = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc "Causes debug information to be logged while gitit is running.";
-      };
-
-      compressResponses = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc "Specifies whether HTTP responses should be compressed.";
-      };
-
-      mimeTypesFile = mkOption {
-        type = types.path;
-        default = "/etc/mime/types.info";
-        description = lib.mdDoc ''
-          Specifies the path of a file containing mime type mappings.  Each
-          line of the file should contain two fields, separated by whitespace.
-          The first field is the mime type, the second is a file extension.
-          For example:
-          ```
-          video/x-ms-wmx  wmx
-          ```
-          If the file is not found, some simple defaults will be used.
-        '';
-      };
-
-      useReCaptcha = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          If true, causes gitit to use the reCAPTCHA service
-          (http://recaptcha.net) to prevent bots from creating accounts.
-        '';
-      };
-
-      reCaptchaPrivateKey = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          Specifies the private key for the reCAPTCHA service.  To get
-          these, you need to create an account at http://recaptcha.net.
-        '';
-      };
-
-      reCaptchaPublicKey = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          Specifies the public key for the reCAPTCHA service.  To get
-          these, you need to create an account at http://recaptcha.net.
-        '';
-      };
-
-      accessQuestion = mkOption {
-        type = types.str;
-        default = "What is the code given to you by Ms. X?";
-        description = lib.mdDoc ''
-          Specifies a question that users must answer when they attempt to
-          create an account
-        '';
-      };
-
-      accessQuestionAnswers = mkOption {
-        type = types.str;
-        default = "RED DOG, red dog";
-        description = lib.mdDoc ''
-          Specifies a question that users must answer when they attempt to
-          create an account, along with a comma-separated list of acceptable
-          answers.  This can be used to institute a rudimentary password for
-          signing up as a user on the wiki, or as an alternative to reCAPTCHA.
-          Example:
-          access-question:  What is the code given to you by Ms. X?
-          access-question-answers:  RED DOG, red dog
-        '';
-      };
-
-      rpxDomain = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          Specifies the domain and key of your RPX account.  The domain is just
-          the prefix of the complete RPX domain, so if your full domain is
-          'https://foo.rpxnow.com/', use 'foo' as the value of rpx-domain.
-        '';
-      };
-
-      rpxKey = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "RPX account access key.";
-      };
-
-      mailCommand = mkOption {
-        type = types.str;
-        default = "sendmail %s";
-        description = lib.mdDoc ''
-          Specifies the command to use to send notification emails.  '%s' will
-          be replaced by the destination email address.  The body of the
-          message will be read from stdin.  If this field is left blank,
-          password reset will not be offered.
-        '';
-      };
-
-      resetPasswordMessage = mkOption {
-        type = types.lines;
-        default = ''
-          > From: gitit@$hostname$
-          > To: $useremail$
-          > Subject: Wiki password reset
-          >
-          > Hello $username$,
-          >
-          > To reset your password, please follow the link below:
-          > http://$hostname$:$port$$resetlink$
-          >
-          > Regards
-        '';
-        description = lib.mdDoc ''
-          Gives the text of the message that will be sent to the user should
-          she want to reset her password, or change other registration info.
-          The lines must be indented, and must begin with '>'.  The initial
-          spaces and '> ' will be stripped off.  $username$ will be replaced by
-          the user's username, $useremail$ by her email address, $hostname$ by
-          the hostname on which the wiki is running (as returned by the
-          hostname system call), $port$ by the port on which the wiki is
-          running, and $resetlink$ by the relative path of a reset link derived
-          from the user's existing hashed password. If your gitit wiki is being
-          proxied to a location other than the root path of $port$, you should
-          change the link to reflect this: for example, to
-          http://$hostname$/path/to/wiki$resetlink$ or
-          http://gitit.$hostname$$resetlink$
-        '';
-      };
-
-      useFeed = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether an ATOM feed should be enabled (for the site and
-          for individual pages).
-        '';
-      };
-
-      baseUrl = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc ''
-          The base URL of the wiki, to be used in constructing feed IDs and RPX
-          token_urls.  Set this if useFeed is false or authentication-method
-          is 'rpx'.
-        '';
-      };
-
-      absoluteUrls = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Make wikilinks absolute with respect to the base-url.  So, for
-          example, in a wiki served at the base URL '/wiki', on a page
-          Sub/Page, the wikilink `[Cactus]()` will produce a link to
-          '/wiki/Cactus' if absoluteUrls is true, and a relative link to
-          'Cactus' (referring to '/wiki/Sub/Cactus') if absolute-urls is 'no'.
-        '';
-      };
-
-      feedDays = mkOption {
-        type = types.int;
-        default = 14;
-        description = lib.mdDoc "Number of days to be included in feeds.";
-      };
-
-      feedRefreshTime = mkOption {
-        type = types.int;
-        default = 60;
-        description = lib.mdDoc "Number of minutes to cache feeds before refreshing.";
-      };
-
-      pdfExport = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          If true, PDF will appear in export options. PDF will be created using
-          pdflatex, which must be installed and in the path. Note that PDF
-          exports create significant additional server load.
-        '';
-      };
-
-      pandocUserData = mkOption {
-        type = with types; nullOr path;
-        default = null;
-        description = lib.mdDoc ''
-          If a directory is specified, this will be searched for pandoc
-          customizations. These can include a templates/ directory for custom
-          templates for various export formats, an S5 directory for custom S5
-          styles, and a reference.odt for ODT exports. If no directory is
-          specified, $HOME/.pandoc will be searched. See pandoc's README for
-          more information.
-        '';
-      };
-
-      xssSanitize = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          If true, all HTML (including that produced by pandoc) is filtered
-          through xss-sanitize.  Set to no only if you trust all of your users.
-        '';
-      };
-
-      oauthClientId = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth client ID";
-      };
-
-      oauthClientSecret = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth client secret";
-      };
-
-      oauthCallback = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth callback URL";
-      };
-
-      oauthAuthorizeEndpoint = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth authorize endpoint";
-      };
-
-      oauthAccessTokenEndpoint = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "OAuth access token endpoint";
-      };
-
-      githubOrg = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = lib.mdDoc "Github organization";
-      };
-  };
-
-  configFile = pkgs.writeText "gitit.conf" ''
-    address: ${cfg.address}
-    port: ${toString cfg.port}
-    wiki-title: ${cfg.wikiTitle}
-    repository-type: ${cfg.repositoryType}
-    repository-path: ${cfg.repositoryPath}
-    require-authentication: ${cfg.requireAuthentication}
-    authentication-method: ${cfg.authenticationMethod}
-    user-file: ${cfg.userFile}
-    session-timeout: ${toString cfg.sessionTimeout}
-    static-dir: ${cfg.staticDir}
-    default-page-type: ${cfg.defaultPageType}
-    math: ${cfg.math}
-    mathjax-script: ${cfg.mathJaxScript}
-    show-lhs-bird-tracks: ${toYesNo cfg.showLhsBirdTracks}
-    templates-dir: ${cfg.templatesDir}
-    log-file: ${cfg.logFile}
-    log-level: ${cfg.logLevel}
-    front-page: ${cfg.frontPage}
-    no-delete: ${cfg.noDelete}
-    no-edit: ${cfg.noEdit}
-    default-summary: ${cfg.defaultSummary}
-    table-of-contents: ${toYesNo cfg.tableOfContents}
-    plugins: ${concatStringsSep "," cfg.plugins}
-    use-cache: ${toYesNo cfg.useCache}
-    cache-dir: ${cfg.cacheDir}
-    max-upload-size: ${cfg.maxUploadSize}
-    max-page-size: ${cfg.maxPageSize}
-    debug-mode: ${toYesNo cfg.debugMode}
-    compress-responses: ${toYesNo cfg.compressResponses}
-    mime-types-file: ${cfg.mimeTypesFile}
-    use-recaptcha: ${toYesNo cfg.useReCaptcha}
-    recaptcha-private-key: ${toString cfg.reCaptchaPrivateKey}
-    recaptcha-public-key: ${toString cfg.reCaptchaPublicKey}
-    access-question: ${cfg.accessQuestion}
-    access-question-answers: ${cfg.accessQuestionAnswers}
-    rpx-domain: ${toString cfg.rpxDomain}
-    rpx-key: ${toString cfg.rpxKey}
-    mail-command: ${cfg.mailCommand}
-    reset-password-message: ${cfg.resetPasswordMessage}
-    use-feed: ${toYesNo cfg.useFeed}
-    base-url: ${toString cfg.baseUrl}
-    absolute-urls: ${toYesNo cfg.absoluteUrls}
-    feed-days: ${toString cfg.feedDays}
-    feed-refresh-time: ${toString cfg.feedRefreshTime}
-    pdf-export: ${toYesNo cfg.pdfExport}
-    pandoc-user-data: ${toString cfg.pandocUserData}
-    xss-sanitize: ${toYesNo cfg.xssSanitize}
-
-    [Github]
-    oauthclientid: ${toString cfg.oauthClientId}
-    oauthclientsecret: ${toString cfg.oauthClientSecret}
-    oauthcallback: ${toString cfg.oauthCallback}
-    oauthauthorizeendpoint: ${toString cfg.oauthAuthorizeEndpoint}
-    oauthaccesstokenendpoint: ${toString cfg.oauthAccessTokenEndpoint}
-    github-org: ${toString cfg.githubOrg}
-  '';
-
-in
-
-{
-
-  options.services.gitit = gititOptions;
-
-  config = mkIf cfg.enable {
-
-    users.users.gitit = {
-      group = config.users.groups.gitit.name;
-      description = "Gitit user";
-      home = homeDir;
-      createHome = true;
-      uid = config.ids.uids.gitit;
-    };
-
-    users.groups.gitit.gid = config.ids.gids.gitit;
-
-    systemd.services.gitit = let
-      uid = toString config.ids.uids.gitit;
-      gid = toString config.ids.gids.gitit;
-    in {
-      description = "Git and Pandoc Powered Wiki";
-      after = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ curl ]
-             ++ optional cfg.pdfExport texlive.combined.scheme-basic
-             ++ optional (cfg.repositoryType == "darcs") darcs
-             ++ optional (cfg.repositoryType == "mercurial") mercurial
-             ++ optional (cfg.repositoryType == "git") git;
-
-      preStart = let
-        gm = "gitit@${config.networking.hostName}";
-      in
-      with cfg; ''
-        chown ${uid}:${gid} -R ${homeDir}
-        for dir in ${repositoryPath} ${staticDir} ${templatesDir} ${cacheDir}
-        do
-          if [ ! -d $dir ]
-          then
-            mkdir -p $dir
-            find $dir -type d -exec chmod 0750 {} +
-            find $dir -type f -exec chmod 0640 {} +
-          fi
-        done
-        cd ${repositoryPath}
-        ${
-          if repositoryType == "darcs" then
-          ''
-          if [ ! -d _darcs ]
-          then
-            darcs initialize
-            echo "${gm}" > _darcs/prefs/email
-          ''
-          else if repositoryType == "mercurial" then
-          ''
-          if [ ! -d .hg ]
-          then
-            hg init
-            cat >> .hg/hgrc <<NAMED
-[ui]
-username = gitit ${gm}
-NAMED
-          ''
-          else
-          ''
-          if [ ! -d  .git ]
-          then
-            git init
-            git config user.email "${gm}"
-            git config user.name "gitit"
-          ''}
-          chown ${uid}:${gid} -R ${repositoryPath}
-          fi
-        cd -
-      '';
-
-      serviceConfig = {
-        User = config.users.users.gitit.name;
-        Group = config.users.groups.gitit.name;
-        ExecStart = with cfg; gititSh haskellPackages extraPackages;
-      };
-    };
-  };
-}
diff --git a/nixos/modules/services/misc/gitlab.md b/nixos/modules/services/misc/gitlab.md
new file mode 100644
index 00000000000..916b23584ed
--- /dev/null
+++ b/nixos/modules/services/misc/gitlab.md
@@ -0,0 +1,112 @@
+# GitLab {#module-services-gitlab}
+
+GitLab is a feature-rich git hosting service.
+
+## Prerequisites {#module-services-gitlab-prerequisites}
+
+The `gitlab` service exposes only an Unix socket at
+`/run/gitlab/gitlab-workhorse.socket`. You need to
+configure a webserver to proxy HTTP requests to the socket.
+
+For instance, the following configuration could be used to use nginx as
+frontend proxy:
+```
+services.nginx = {
+  enable = true;
+  recommendedGzipSettings = true;
+  recommendedOptimisation = true;
+  recommendedProxySettings = true;
+  recommendedTlsSettings = true;
+  virtualHosts."git.example.com" = {
+    enableACME = true;
+    forceSSL = true;
+    locations."/".proxyPass = "http://unix:/run/gitlab/gitlab-workhorse.socket";
+  };
+};
+```
+
+## Configuring {#module-services-gitlab-configuring}
+
+GitLab depends on both PostgreSQL and Redis and will automatically enable
+both services. In the case of PostgreSQL, a database and a role will be
+created.
+
+The default state dir is `/var/gitlab/state`. This is where
+all data like the repositories and uploads will be stored.
+
+A basic configuration with some custom settings could look like this:
+```
+services.gitlab = {
+  enable = true;
+  databasePasswordFile = "/var/keys/gitlab/db_password";
+  initialRootPasswordFile = "/var/keys/gitlab/root_password";
+  https = true;
+  host = "git.example.com";
+  port = 443;
+  user = "git";
+  group = "git";
+  smtp = {
+    enable = true;
+    address = "localhost";
+    port = 25;
+  };
+  secrets = {
+    dbFile = "/var/keys/gitlab/db";
+    secretFile = "/var/keys/gitlab/secret";
+    otpFile = "/var/keys/gitlab/otp";
+    jwsFile = "/var/keys/gitlab/jws";
+  };
+  extraConfig = {
+    gitlab = {
+      email_from = "gitlab-no-reply@example.com";
+      email_display_name = "Example GitLab";
+      email_reply_to = "gitlab-no-reply@example.com";
+      default_projects_features = { builds = false; };
+    };
+  };
+};
+```
+
+If you're setting up a new GitLab instance, generate new
+secrets. You for instance use
+`tr -dc A-Za-z0-9 < /dev/urandom | head -c 128 > /var/keys/gitlab/db` to
+generate a new db secret. Make sure the files can be read by, and
+only by, the user specified by
+[services.gitlab.user](#opt-services.gitlab.user). GitLab
+encrypts sensitive data stored in the database. If you're restoring
+an existing GitLab instance, you must specify the secrets secret
+from `config/secrets.yml` located in your GitLab
+state folder.
+
+When `incoming_mail.enabled` is set to `true`
+in [extraConfig](#opt-services.gitlab.extraConfig) an additional
+service called `gitlab-mailroom` is enabled for fetching incoming mail.
+
+Refer to [](#ch-options) for all available configuration
+options for the [services.gitlab](#opt-services.gitlab.enable) module.
+
+## Maintenance {#module-services-gitlab-maintenance}
+
+### Backups {#module-services-gitlab-maintenance-backups}
+
+Backups can be configured with the options in
+[services.gitlab.backup](#opt-services.gitlab.backup.keepTime). Use
+the [services.gitlab.backup.startAt](#opt-services.gitlab.backup.startAt)
+option to configure regular backups.
+
+To run a manual backup, start the `gitlab-backup` service:
+```ShellSession
+$ systemctl start gitlab-backup.service
+```
+
+### Rake tasks {#module-services-gitlab-maintenance-rake}
+
+You can run GitLab's rake tasks with `gitlab-rake`
+which will be available on the system when GitLab is enabled. You
+will have to run the command as the user that you configured to run
+GitLab with.
+
+A list of all available rake tasks can be obtained by running:
+```ShellSession
+$ sudo -u git -H gitlab-rake -T
+```
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
index e7c707228f1..12c67c5f5a1 100644
--- a/nixos/modules/services/misc/gitlab.nix
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -40,6 +40,7 @@ let
 
   gitalyToml = pkgs.writeText "gitaly.toml" ''
     socket_path = "${lib.escape ["\""] gitalySocket}"
+    runtime_dir = "/run/gitaly"
     bin_dir = "${cfg.packages.gitaly}/bin"
     prometheus_listen_addr = "localhost:9236"
 
@@ -88,11 +89,6 @@ let
     };
   };
 
-  pagesArgs = [
-    "-pages-domain" gitlabConfig.production.pages.host
-    "-pages-root" "${gitlabConfig.production.shared.path}/pages"
-  ] ++ cfg.pagesExtraArgs;
-
   gitlabConfig = {
     # These are the default settings from config/gitlab.example.yml
     production = flip recursiveUpdate cfg.extraConfig {
@@ -160,6 +156,12 @@ let
       };
       extra = {};
       uploads.storage_path = cfg.statePath;
+      pages = optionalAttrs cfg.pages.enable {
+        enabled = cfg.pages.enable;
+        port = 8090;
+        host = cfg.pages.settings.pages-domain;
+        secret_file = cfg.pages.settings.api-secret-key;
+      };
     };
   };
 
@@ -245,6 +247,7 @@ in {
     (mkRenamedOptionModule [ "services" "gitlab" "backupPath" ] [ "services" "gitlab" "backup" "path" ])
     (mkRemovedOptionModule [ "services" "gitlab" "satelliteDir" ] "")
     (mkRemovedOptionModule [ "services" "gitlab" "logrotate" "extraConfig" ] "Modify services.logrotate.settings.gitlab directly instead")
+    (mkRemovedOptionModule [ "services" "gitlab" "pagesExtraArgs" ] "Use services.gitlab.pages.settings instead")
   ];
 
   options = {
@@ -666,10 +669,127 @@ in {
         };
       };
 
-      pagesExtraArgs = mkOption {
-        type = types.listOf types.str;
-        default = [ "-listen-proxy" "127.0.0.1:8090" ];
-        description = lib.mdDoc "Arguments to pass to the gitlab-pages daemon";
+      pages.enable = mkEnableOption (lib.mdDoc "the GitLab Pages service");
+
+      pages.settings = mkOption {
+        example = literalExpression ''
+          {
+            pages-domain = "example.com";
+            auth-client-id = "generated-id-xxxxxxx";
+            auth-client-secret = { _secret = "/var/keys/auth-client-secret"; };
+            auth-redirect-uri = "https://projects.example.com/auth";
+            auth-secret = { _secret = "/var/keys/auth-secret"; };
+            auth-server = "https://gitlab.example.com";
+          }
+        '';
+
+        description = lib.mdDoc ''
+          Configuration options to set in the GitLab Pages config
+          file.
+
+          Options containing secret data should be set to an attribute
+          set containing the attribute `_secret` - a string pointing
+          to a file containing the value the option should be set
+          to. See the example to get a better picture of this: in the
+          resulting configuration file, the `auth-client-secret` and
+          `auth-secret` keys will be set to the contents of the
+          {file}`/var/keys/auth-client-secret` and
+          {file}`/var/keys/auth-secret` files respectively.
+        '';
+
+        type = types.submodule {
+          freeformType = with types; attrsOf (nullOr (oneOf [ str int bool attrs ]));
+
+          options = {
+            listen-http = mkOption {
+              type = with types; listOf str;
+              apply = x: if x == [] then null else lib.concatStringsSep "," x;
+              default = [];
+              description = lib.mdDoc ''
+                The address(es) to listen on for HTTP requests.
+              '';
+            };
+
+            listen-https = mkOption {
+              type = with types; listOf str;
+              apply = x: if x == [] then null else lib.concatStringsSep "," x;
+              default = [];
+              description = lib.mdDoc ''
+                The address(es) to listen on for HTTPS requests.
+              '';
+            };
+
+            listen-proxy = mkOption {
+              type = with types; listOf str;
+              apply = x: if x == [] then null else lib.concatStringsSep "," x;
+              default = [ "127.0.0.1:8090" ];
+              description = lib.mdDoc ''
+                The address(es) to listen on for proxy requests.
+              '';
+            };
+
+            artifacts-server = mkOption {
+              type = with types; nullOr str;
+              default = "http${optionalString cfg.https "s"}://${cfg.host}/api/v4";
+              defaultText = "http(s)://<services.gitlab.host>/api/v4";
+              example = "https://gitlab.example.com/api/v4";
+              description = lib.mdDoc ''
+                API URL to proxy artifact requests to.
+              '';
+            };
+
+            gitlab-server = mkOption {
+              type = with types; nullOr str;
+              default = "http${optionalString cfg.https "s"}://${cfg.host}";
+              defaultText = "http(s)://<services.gitlab.host>";
+              example = "https://gitlab.example.com";
+              description = lib.mdDoc ''
+                Public GitLab server URL.
+              '';
+            };
+
+            internal-gitlab-server = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              defaultText = "http(s)://<services.gitlab.host>";
+              example = "https://gitlab.example.internal";
+              description = lib.mdDoc ''
+                Internal GitLab server used for API requests, useful
+                if you want to send that traffic over an internal load
+                balancer. By default, the value of
+                `services.gitlab.pages.settings.gitlab-server` is
+                used.
+              '';
+            };
+
+            api-secret-key = mkOption {
+              type = with types; nullOr str;
+              default = "${cfg.statePath}/gitlab_pages_secret";
+              internal = true;
+              description = lib.mdDoc ''
+                File with secret key used to authenticate with the
+                GitLab API.
+              '';
+            };
+
+            pages-domain = mkOption {
+              type = with types; nullOr str;
+              example = "example.com";
+              description = lib.mdDoc ''
+                The domain to serve static pages on.
+              '';
+            };
+
+            pages-root = mkOption {
+              type = types.str;
+              default = "${gitlabConfig.production.shared.path}/pages";
+              defaultText = literalExpression ''config.${opt.extraConfig}.production.shared.path + "/pages"'';
+              description = lib.mdDoc ''
+                The directory where pages are stored.
+              '';
+            };
+          };
+        };
       };
 
       secrets.secretFile = mkOption {
@@ -1095,7 +1215,7 @@ in {
       enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly
       extraConfig = {
         auth.token = {
-          realm = "http${if cfg.https == true then "s" else ""}://${cfg.host}/jwt/auth";
+          realm = "http${optionalString (cfg.https == true) "s"}://${cfg.host}/jwt/auth";
           service = cfg.registry.serviceName;
           issuer = cfg.registry.issuer;
           rootcertbundle = cfg.registry.certFile;
@@ -1209,6 +1329,9 @@ in {
             umask u=rwx,g=,o=
 
             openssl rand -hex 32 > ${cfg.statePath}/gitlab_shell_secret
+            ${optionalString cfg.pages.enable ''
+                openssl rand -base64 32 > ${cfg.pages.settings.api-secret-key}
+            ''}
 
             rm -f '${cfg.statePath}/config/database.yml'
 
@@ -1353,32 +1476,71 @@ in {
         TimeoutSec = "infinity";
         Restart = "on-failure";
         WorkingDirectory = gitlabEnv.HOME;
+        RuntimeDirectory = "gitaly";
         ExecStart = "${cfg.packages.gitaly}/bin/gitaly ${gitalyToml}";
       };
     };
 
-    systemd.services.gitlab-pages = mkIf (gitlabConfig.production.pages.enabled or false) {
-      description = "GitLab static pages daemon";
-      after = [ "network.target" "gitlab-config.service" ];
-      bindsTo = [ "gitlab-config.service" ];
-      wantedBy = [ "gitlab.target" ];
-      partOf = [ "gitlab.target" ];
-
-      path = [ pkgs.unzip ];
-
-      serviceConfig = {
-        Type = "simple";
-        TimeoutSec = "infinity";
-        Restart = "on-failure";
-
-        User = cfg.user;
-        Group = cfg.group;
-
-        ExecStart = "${cfg.packages.pages}/bin/gitlab-pages ${escapeShellArgs pagesArgs}";
-        WorkingDirectory = gitlabEnv.HOME;
-      };
+    services.gitlab.pages.settings = {
+      api-secret-key = "${cfg.statePath}/gitlab_pages_secret";
     };
 
+    systemd.services.gitlab-pages =
+      let
+        filteredConfig = filterAttrs (_: v: v != null) cfg.pages.settings;
+        isSecret = v: isAttrs v && v ? _secret && isString v._secret;
+        mkPagesKeyValue = lib.generators.toKeyValue {
+          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 builtins.hashString "sha256" v._secret
+              else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}";
+          };
+        };
+        secretPaths = lib.catAttrs "_secret" (lib.collect isSecret filteredConfig);
+        mkSecretReplacement = file: ''
+          replace-secret ${lib.escapeShellArgs [ (builtins.hashString "sha256" file) file "/run/gitlab-pages/gitlab-pages.conf" ]}
+        '';
+        secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
+        configFile = pkgs.writeText "gitlab-pages.conf" (mkPagesKeyValue filteredConfig);
+      in
+        mkIf cfg.pages.enable {
+          description = "GitLab static pages daemon";
+          after = [ "network.target" "gitlab-config.service" "gitlab.service" ];
+          bindsTo = [ "gitlab-config.service" "gitlab.service" ];
+          wantedBy = [ "gitlab.target" ];
+          partOf = [ "gitlab.target" ];
+
+          path = with pkgs; [
+            unzip
+            replace-secret
+          ];
+
+          serviceConfig = {
+            Type = "simple";
+            TimeoutSec = "infinity";
+            Restart = "on-failure";
+
+            User = cfg.user;
+            Group = cfg.group;
+
+            ExecStartPre = pkgs.writeShellScript "gitlab-pages-pre-start" ''
+              set -o errexit -o pipefail -o nounset
+              shopt -s dotglob nullglob inherit_errexit
+
+              install -m u=rw ${configFile} /run/gitlab-pages/gitlab-pages.conf
+              ${secretReplacements}
+            '';
+            ExecStart = "${cfg.packages.pages}/bin/gitlab-pages -config=/run/gitlab-pages/gitlab-pages.conf";
+            WorkingDirectory = gitlabEnv.HOME;
+            RuntimeDirectory = "gitlab-pages";
+            RuntimeDirectoryMode = "0700";
+          };
+        };
+
     systemd.services.gitlab-workhorse = {
       after = [ "network.target" ];
       wantedBy = [ "gitlab.target" ];
@@ -1502,6 +1664,6 @@ in {
 
   };
 
-  meta.doc = ./gitlab.xml;
+  meta.doc = ./gitlab.md;
 
 }
diff --git a/nixos/modules/services/misc/gitlab.xml b/nixos/modules/services/misc/gitlab.xml
deleted file mode 100644
index 9816fdac7dd..00000000000
--- a/nixos/modules/services/misc/gitlab.xml
+++ /dev/null
@@ -1,151 +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-gitlab">
- <title>GitLab</title>
- <para>
-  GitLab is a feature-rich git hosting service.
- </para>
- <section xml:id="module-services-gitlab-prerequisites">
-  <title>Prerequisites</title>
-
-  <para>
-   The <literal>gitlab</literal> service exposes only an Unix socket at
-   <literal>/run/gitlab/gitlab-workhorse.socket</literal>. You need to
-   configure a webserver to proxy HTTP requests to the socket.
-  </para>
-
-  <para>
-   For instance, the following configuration could be used to use nginx as
-   frontend proxy:
-<programlisting>
-<link linkend="opt-services.nginx.enable">services.nginx</link> = {
-  <link linkend="opt-services.nginx.enable">enable</link> = true;
-  <link linkend="opt-services.nginx.recommendedGzipSettings">recommendedGzipSettings</link> = true;
-  <link linkend="opt-services.nginx.recommendedOptimisation">recommendedOptimisation</link> = true;
-  <link linkend="opt-services.nginx.recommendedProxySettings">recommendedProxySettings</link> = true;
-  <link linkend="opt-services.nginx.recommendedTlsSettings">recommendedTlsSettings</link> = true;
-  <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link>."git.example.com" = {
-    <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_.proxyPass">locations."/".proxyPass</link> = "http://unix:/run/gitlab/gitlab-workhorse.socket";
-  };
-};
-</programlisting>
-  </para>
- </section>
- <section xml:id="module-services-gitlab-configuring">
-  <title>Configuring</title>
-
-  <para>
-   GitLab depends on both PostgreSQL and Redis and will automatically enable
-   both services. In the case of PostgreSQL, a database and a role will be
-   created.
-  </para>
-
-  <para>
-   The default state dir is <literal>/var/gitlab/state</literal>. This is where
-   all data like the repositories and uploads will be stored.
-  </para>
-
-  <para>
-   A basic configuration with some custom settings could look like this:
-<programlisting>
-services.gitlab = {
-  <link linkend="opt-services.gitlab.enable">enable</link> = true;
-  <link linkend="opt-services.gitlab.databasePasswordFile">databasePasswordFile</link> = "/var/keys/gitlab/db_password";
-  <link linkend="opt-services.gitlab.initialRootPasswordFile">initialRootPasswordFile</link> = "/var/keys/gitlab/root_password";
-  <link linkend="opt-services.gitlab.https">https</link> = true;
-  <link linkend="opt-services.gitlab.host">host</link> = "git.example.com";
-  <link linkend="opt-services.gitlab.port">port</link> = 443;
-  <link linkend="opt-services.gitlab.user">user</link> = "git";
-  <link linkend="opt-services.gitlab.group">group</link> = "git";
-  smtp = {
-    <link linkend="opt-services.gitlab.smtp.enable">enable</link> = true;
-    <link linkend="opt-services.gitlab.smtp.address">address</link> = "localhost";
-    <link linkend="opt-services.gitlab.smtp.port">port</link> = 25;
-  };
-  secrets = {
-    <link linkend="opt-services.gitlab.secrets.dbFile">dbFile</link> = "/var/keys/gitlab/db";
-    <link linkend="opt-services.gitlab.secrets.secretFile">secretFile</link> = "/var/keys/gitlab/secret";
-    <link linkend="opt-services.gitlab.secrets.otpFile">otpFile</link> = "/var/keys/gitlab/otp";
-    <link linkend="opt-services.gitlab.secrets.jwsFile">jwsFile</link> = "/var/keys/gitlab/jws";
-  };
-  <link linkend="opt-services.gitlab.extraConfig">extraConfig</link> = {
-    gitlab = {
-      email_from = "gitlab-no-reply@example.com";
-      email_display_name = "Example GitLab";
-      email_reply_to = "gitlab-no-reply@example.com";
-      default_projects_features = { builds = false; };
-    };
-  };
-};
-</programlisting>
-  </para>
-
-  <para>
-   If you're setting up a new GitLab instance, generate new
-   secrets. You for instance use <literal>tr -dc A-Za-z0-9 &lt;
-   /dev/urandom | head -c 128 &gt; /var/keys/gitlab/db</literal> to
-   generate a new db secret. Make sure the files can be read by, and
-   only by, the user specified by <link
-   linkend="opt-services.gitlab.user">services.gitlab.user</link>. GitLab
-   encrypts sensitive data stored in the database. If you're restoring
-   an existing GitLab instance, you must specify the secrets secret
-   from <literal>config/secrets.yml</literal> located in your GitLab
-   state folder.
-  </para>
-
-  <para>
-    When <literal>incoming_mail.enabled</literal> is set to <literal>true</literal>
-    in <link linkend="opt-services.gitlab.extraConfig">extraConfig</link> an additional
-    service called <literal>gitlab-mailroom</literal> is enabled for fetching incoming mail.
-  </para>
-
-  <para>
-   Refer to <xref linkend="ch-options" /> for all available configuration
-   options for the
-   <link linkend="opt-services.gitlab.enable">services.gitlab</link> module.
-  </para>
- </section>
- <section xml:id="module-services-gitlab-maintenance">
-  <title>Maintenance</title>
-
-  <section xml:id="module-services-gitlab-maintenance-backups">
-   <title>Backups</title>
-   <para>
-     Backups can be configured with the options in <link
-     linkend="opt-services.gitlab.backup.keepTime">services.gitlab.backup</link>. Use
-     the <link
-     linkend="opt-services.gitlab.backup.startAt">services.gitlab.backup.startAt</link>
-     option to configure regular backups.
-   </para>
-
-   <para>
-     To run a manual backup, start the <literal>gitlab-backup</literal> service:
-<screen>
-<prompt>$ </prompt>systemctl start gitlab-backup.service
-</screen>
-   </para>
-  </section>
-
-  <section xml:id="module-services-gitlab-maintenance-rake">
-   <title>Rake tasks</title>
-
-   <para>
-    You can run GitLab's rake tasks with <literal>gitlab-rake</literal>
-    which will be available on the system when GitLab is enabled. You
-    will have to run the command as the user that you configured to run
-    GitLab with.
-   </para>
-
-   <para>
-    A list of all available rake tasks can be obtained by running:
-<screen>
-<prompt>$ </prompt>sudo -u git -H gitlab-rake -T
-</screen>
-   </para>
-  </section>
- </section>
-</chapter>
diff --git a/nixos/modules/services/misc/gpsd.nix b/nixos/modules/services/misc/gpsd.nix
index ec0a8e1eaa1..ce0f9bb3ba2 100644
--- a/nixos/modules/services/misc/gpsd.nix
+++ b/nixos/modules/services/misc/gpsd.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, ... }:
+{ config, lib, pkgs, utils, ... }:
 
 with lib;
 
@@ -8,12 +8,15 @@ let
   gid = config.ids.gids.gpsd;
   cfg = config.services.gpsd;
 
-in
-
-{
+in {
 
   ###### interface
 
+  imports = [
+    (lib.mkRemovedOptionModule [ "services" "gpsd" "device" ]
+      "Use `services.gpsd.devices` instead.")
+  ];
+
   options = {
 
     services.gpsd = {
@@ -22,17 +25,21 @@ in
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Whether to enable `gpsd', a GPS service daemon.
+          Whether to enable `gpsd`, a GPS service daemon.
         '';
       };
 
-      device = mkOption {
-        type = types.str;
-        default = "/dev/ttyUSB0";
+      devices = mkOption {
+        type = types.listOf types.str;
+        default = [ "/dev/ttyUSB0" ];
         description = lib.mdDoc ''
-          A device may be a local serial device for GPS input, or a URL of the form:
-               `[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]`
-          in which case it specifies an input source for DGPS or ntrip data.
+          List of devices that `gpsd` should subscribe to.
+
+          A device may be a local serial device for GPS input, or a
+          URL of the form:
+          `[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]` in
+          which case it specifies an input source for DGPS or ntrip
+          data.
         '';
       };
 
@@ -89,17 +96,16 @@ in
 
   };
 
-
   ###### implementation
 
   config = mkIf cfg.enable {
 
-    users.users.gpsd =
-      { inherit uid;
-        group = "gpsd";
-        description = "gpsd daemon user";
-        home = "/var/empty";
-      };
+    users.users.gpsd = {
+      inherit uid;
+      group = "gpsd";
+      description = "gpsd daemon user";
+      home = "/var/empty";
+    };
 
     users.groups.gpsd = { inherit gid; };
 
@@ -109,13 +115,15 @@ in
       after = [ "network.target" ];
       serviceConfig = {
         Type = "forking";
-        ExecStart = ''
+        ExecStart = let
+          devices = utils.escapeSystemdExecArgs cfg.devices;
+        in ''
           ${pkgs.gpsd}/sbin/gpsd -D "${toString cfg.debugLevel}"  \
             -S "${toString cfg.port}"                             \
             ${optionalString cfg.readonly "-b"}                   \
             ${optionalString cfg.nowait "-n"}                     \
             ${optionalString cfg.listenany "-G"}                  \
-            "${cfg.device}"
+            ${devices}
         '';
       };
     };
diff --git a/nixos/modules/services/misc/input-remapper.nix b/nixos/modules/services/misc/input-remapper.nix
index 51e1abdc98a..3f6d97f8573 100644
--- a/nixos/modules/services/misc/input-remapper.nix
+++ b/nixos/modules/services/misc/input-remapper.nix
@@ -6,8 +6,8 @@ let cfg = config.services.input-remapper; in
 {
   options = {
     services.input-remapper = {
-      enable = mkEnableOption (lib.mdDoc "input-remapper, an easy to use tool to change the mapping of your input device buttons.");
-      package = options.mkPackageOption pkgs "input-remapper" { };
+      enable = mkEnableOption (lib.mdDoc "input-remapper, an easy to use tool to change the mapping of your input device buttons");
+      package = mkPackageOptionMD pkgs "input-remapper" { };
       enableUdevRules = mkEnableOption (lib.mdDoc "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" ];
diff --git a/nixos/modules/services/misc/jellyseerr.nix b/nixos/modules/services/misc/jellyseerr.nix
new file mode 100644
index 00000000000..31e0c5beb67
--- /dev/null
+++ b/nixos/modules/services/misc/jellyseerr.nix
@@ -0,0 +1,62 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+  cfg = config.services.jellyseerr;
+in
+{
+  meta.maintainers = [ maintainers.camillemndn ];
+
+  options.services.jellyseerr = {
+    enable = mkEnableOption (mdDoc ''Jellyseerr, a requests manager for Jellyfin'');
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc ''Open port in the firewall for the Jellyseerr web interface.'';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 5055;
+      description = mdDoc ''The port which the Jellyseerr web UI should listen to.'';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.jellyseerr = {
+      description = "Jellyseerr, a requests manager for Jellyfin";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment.PORT = toString cfg.port;
+      serviceConfig = {
+        Type = "exec";
+        StateDirectory = "jellyseerr";
+        WorkingDirectory = "${pkgs.jellyseerr}/libexec/jellyseerr/deps/jellyseerr";
+        DynamicUser = true;
+        ExecStart = "${pkgs.jellyseerr}/bin/jellyseerr";
+        BindPaths = [ "/var/lib/jellyseerr/:${pkgs.jellyseerr}/libexec/jellyseerr/deps/jellyseerr/config/" ];
+        Restart = "on-failure";
+        ProtectHome = true;
+        ProtectSystem = "strict";
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        NoNewPrivileges = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        PrivateMounts = true;
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ cfg.port ];
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/klipper.nix b/nixos/modules/services/misc/klipper.nix
index a2158e9461b..9f8539980aa 100644
--- a/nixos/modules/services/misc/klipper.nix
+++ b/nixos/modules/services/misc/klipper.nix
@@ -135,7 +135,7 @@ in
       }
       {
         assertion = (cfg.configFile != null) != (cfg.settings != null);
-        message = "You need to either specify services.klipper.settings or services.klipper.defaultConfig.";
+        message = "You need to either specify services.klipper.settings or services.klipper.configFile.";
       }
     ];
 
diff --git a/nixos/modules/services/misc/mbpfan.nix b/nixos/modules/services/misc/mbpfan.nix
index d467aa87976..e75c3525414 100644
--- a/nixos/modules/services/misc/mbpfan.nix
+++ b/nixos/modules/services/misc/mbpfan.nix
@@ -1,10 +1,9 @@
 { config, lib, pkgs, ... }:
-
 with lib;
 
 let
   cfg = config.services.mbpfan;
-  verbose = if cfg.verbose then "v" else "";
+  verbose = optionalString cfg.verbose "v";
   settingsFormat = pkgs.formats.ini {};
   settingsFile = settingsFormat.generate "mbpfan.ini" cfg.settings;
 
@@ -16,17 +15,19 @@ in {
       type = types.package;
       default = pkgs.mbpfan;
       defaultText = literalExpression "pkgs.mbpfan";
-      description = lib.mdDoc ''
-        The package used for the mbpfan daemon.
-      '';
+      description = lib.mdDoc "The package used for the mbpfan daemon.";
     };
 
     verbose = mkOption {
       type = types.bool;
       default = false;
-      description = lib.mdDoc ''
-        If true, sets the log level to verbose.
-      '';
+      description = lib.mdDoc "If true, sets the log level to verbose.";
+    };
+
+    aggressive = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc "If true, favors higher default fan speeds.";
     };
 
     settings = mkOption {
@@ -35,24 +36,14 @@ in {
       type = types.submodule {
         freeformType = settingsFormat.type;
 
-        options.general.min_fan1_speed = mkOption {
-          type = types.nullOr types.int;
-          default = 2000;
-          description = lib.mdDoc ''
-            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;
+          default = 63;
           description = lib.mdDoc "If temperature is below this, fans will run at minimum speed.";
         };
         options.general.high_temp = mkOption {
           type = types.int;
-          default = 58;
+          default = 66;
           description = lib.mdDoc "If temperature is above this, fan speed will gradually increase.";
         };
         options.general.max_temp = mkOption {
@@ -79,10 +70,16 @@ in {
   ];
 
   config = mkIf cfg.enable {
-    boot.kernelModules = [ "coretemp" "applesmc" ];
+    services.mbpfan.settings = mkIf cfg.aggressive {
+      general.min_fan1_speed = mkDefault 2000;
+      general.low_temp = mkDefault 55;
+      general.high_temp = mkDefault 58;
+      general.max_temp = mkDefault 70;
+    };
 
-    environment.etc."mbpfan.conf".source = settingsFile;
+    boot.kernelModules = [ "coretemp" "applesmc" ];
     environment.systemPackages = [ cfg.package ];
+    environment.etc."mbpfan.conf".source = settingsFile;
 
     systemd.services.mbpfan = {
       description = "A fan manager daemon for MacBook Pro";
diff --git a/nixos/modules/services/misc/moonraker.nix b/nixos/modules/services/misc/moonraker.nix
index 62064b5d90f..7e306d718e0 100644
--- a/nixos/modules/services/misc/moonraker.nix
+++ b/nixos/modules/services/misc/moonraker.nix
@@ -11,6 +11,8 @@ let
       else lib.concatMapStrings (s: "\n  ${generators.mkValueStringDefault {} s}") l;
     mkKeyValue = generators.mkKeyValueDefault {} ":";
   };
+
+  unifiedConfigDir = cfg.stateDir + "/config";
 in {
   options = {
     services.moonraker = {
@@ -30,11 +32,10 @@ in {
       };
 
       configDir = mkOption {
-        type = types.path;
-        default = cfg.stateDir + "/config";
-        defaultText = literalExpression ''config.${opt.stateDir} + "/config"'';
+        type = types.nullOr types.path;
+        default = null;
         description = lib.mdDoc ''
-          The directory containing client-writable configuration files.
+          Deprecated directory containing client-writable configuration files.
 
           Clients will be able to edit files in this directory via the API. This directory must be writable.
         '';
@@ -71,7 +72,7 @@ in {
         example = {
           authorization = {
             trusted_clients = [ "10.0.0.0/24" ];
-            cors_domains = [ "https://app.fluidd.xyz" ];
+            cors_domains = [ "https://app.fluidd.xyz" "https://my.mainsail.xyz" ];
           };
         };
         description = lib.mdDoc ''
@@ -96,8 +97,18 @@ in {
   };
 
   config = mkIf cfg.enable {
-    warnings = optional (cfg.settings ? update_manager)
-      ''Enabling update_manager is not supported on NixOS and will lead to non-removable warnings in some clients.'';
+    warnings = []
+      ++ optional (cfg.settings ? update_manager)
+        ''Enabling update_manager is not supported on NixOS and will lead to non-removable warnings in some clients.''
+      ++ optional (cfg.configDir != null)
+        ''
+          services.moonraker.configDir has been deprecated upstream and will be removed.
+
+          Action: ${
+            if cfg.configDir == unifiedConfigDir then "Simply remove services.moonraker.configDir from your config."
+            else "Move files from `${cfg.configDir}` to `${unifiedConfigDir}` then remove services.moonraker.configDir from your config."
+          }
+        '';
 
     assertions = [
       {
@@ -124,20 +135,20 @@ in {
           port = cfg.port;
           klippy_uds_address = cfg.klipperSocket;
         };
+        machine = {
+          validate_service = false;
+        };
+      } // (lib.optionalAttrs (cfg.configDir != null) {
         file_manager = {
           config_path = cfg.configDir;
         };
-        database = {
-          database_path = "${cfg.stateDir}/database";
-        };
-      };
+      });
       fullConfig = recursiveUpdate cfg.settings forcedConfig;
     in format.generate "moonraker.cfg" fullConfig;
 
     systemd.tmpfiles.rules = [
       "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
-      "d '${cfg.configDir}' - ${cfg.user} ${cfg.group} - -"
-    ];
+    ] ++ lib.optional (cfg.configDir != null) "d '${cfg.configDir}' - ${cfg.user} ${cfg.group} - -";
 
     systemd.services.moonraker = {
       description = "Moonraker, an API web server for Klipper";
@@ -147,9 +158,16 @@ in {
 
       # Moonraker really wants its own config to be writable...
       script = ''
-        cp /etc/moonraker.cfg ${cfg.configDir}/moonraker-temp.cfg
-        chmod u+w ${cfg.configDir}/moonraker-temp.cfg
-        exec ${pkg}/bin/moonraker -c ${cfg.configDir}/moonraker-temp.cfg
+        config_path=${
+          # Deprecated separate config dir
+          if cfg.configDir != null then "${cfg.configDir}/moonraker-temp.cfg"
+          # Config in unified data path
+          else "${unifiedConfigDir}/moonraker-temp.cfg"
+        }
+        mkdir -p $(dirname "$config_path")
+        cp /etc/moonraker.cfg "$config_path"
+        chmod u+w "$config_path"
+        exec ${pkg}/bin/moonraker -d ${cfg.stateDir} -c "$config_path"
       '';
 
       # Needs `ip` command
@@ -184,5 +202,6 @@ in {
   meta.maintainers = with maintainers; [
     cab404
     vtuan10
+    zhaofengli
   ];
 }
diff --git a/nixos/modules/services/misc/n8n.nix b/nixos/modules/services/misc/n8n.nix
index f59df471e1e..cdfe9dc8482 100644
--- a/nixos/modules/services/misc/n8n.nix
+++ b/nixos/modules/services/misc/n8n.nix
@@ -9,7 +9,6 @@ let
 in
 {
   options.services.n8n = {
-
     enable = mkEnableOption (lib.mdDoc "n8n server");
 
     openFirewall = mkOption {
@@ -22,7 +21,7 @@ in
       type = format.type;
       default = {};
       description = lib.mdDoc ''
-        Configuration for n8n, see <https://docs.n8n.io/reference/configuration.html>
+        Configuration for n8n, see <https://docs.n8n.io/hosting/environment-variables/configuration-methods/>
         for supported values.
       '';
     };
@@ -45,6 +44,10 @@ in
         N8N_USER_FOLDER = "/var/lib/n8n";
         HOME = "/var/lib/n8n";
         N8N_CONFIG_FILES = "${configFile}";
+
+        # Don't phone home
+        N8N_DIAGNOSTICS_ENABLED = "false";
+        N8N_VERSION_NOTIFICATIONS_ENABLED = "false";
       };
       serviceConfig = {
         Type = "simple";
diff --git a/nixos/modules/services/misc/nitter.nix b/nixos/modules/services/misc/nitter.nix
index 95394d9d211..2d0d91f9598 100644
--- a/nixos/modules/services/misc/nitter.nix
+++ b/nixos/modules/services/misc/nitter.nix
@@ -47,7 +47,7 @@ in
 {
   options = {
     services.nitter = {
-      enable = mkEnableOption (lib.mdDoc "If enabled, start Nitter.");
+      enable = mkEnableOption (lib.mdDoc "Nitter");
 
       package = mkOption {
         default = pkgs.nitter;
@@ -185,6 +185,13 @@ in
           description = lib.mdDoc "Replace YouTube links with links to this instance (blank to disable).";
         };
 
+        replaceReddit = mkOption {
+          type = types.str;
+          default = "";
+          example = "teddit.net";
+          description = lib.mdDoc "Replace Reddit links with links to this instance (blank to disable).";
+        };
+
         replaceInstagram = mkOption {
           type = types.str;
           default = "";
diff --git a/nixos/modules/services/misc/nix-daemon.nix b/nixos/modules/services/misc/nix-daemon.nix
index f9f0736efcb..f37d197f162 100644
--- a/nixos/modules/services/misc/nix-daemon.nix
+++ b/nixos/modules/services/misc/nix-daemon.nix
@@ -42,7 +42,7 @@ let
         else if isDerivation v then toString v
         else if builtins.isPath v then toString v
         else if isString v then v
-        else if isCoercibleToString v then toString v
+        else if strings.isConvertibleWithToString v then toString v
         else abort "The nix conf value: ${toPretty {} v} can not be encoded";
 
       mkKeyValue = k: v: "${escape [ "=" ] k} = ${mkValueString v}";
@@ -609,7 +609,7 @@ in
 
                 By default, pseudo-features `nixos-test`, `benchmark`,
                 and `big-parallel` used in Nixpkgs are set, `kvm`
-                is also included in it is available.
+                is also included if it is available.
               '';
             };
 
@@ -792,7 +792,10 @@ in
         fi
       '';
 
-    nix.nrBuildUsers = mkDefault (max 32 (if cfg.settings.max-jobs == "auto" then 0 else cfg.settings.max-jobs));
+    nix.nrBuildUsers = mkDefault (
+      if cfg.settings.auto-allocate-uids or false then 0
+      else max 32 (if cfg.settings.max-jobs == "auto" then 0 else cfg.settings.max-jobs)
+    );
 
     users.users = nixbldUsers;
 
@@ -816,10 +819,10 @@ in
 
         system-features = mkDefault (
           [ "nixos-test" "benchmark" "big-parallel" "kvm" ] ++
-          optionals (pkgs.hostPlatform ? gcc.arch) (
+          optionals (pkgs.stdenv.hostPlatform ? gcc.arch) (
             # a builder can run code for `gcc.arch` and inferior architectures
-            [ "gccarch-${pkgs.hostPlatform.gcc.arch}" ] ++
-            map (x: "gccarch-${x}") systems.architectures.inferiors.${pkgs.hostPlatform.gcc.arch}
+            [ "gccarch-${pkgs.stdenv.hostPlatform.gcc.arch}" ] ++
+            map (x: "gccarch-${x}") (systems.architectures.inferiors.${pkgs.stdenv.hostPlatform.gcc.arch} or [])
           )
         );
       }
diff --git a/nixos/modules/services/misc/ntfy-sh.nix b/nixos/modules/services/misc/ntfy-sh.nix
index 9d52fcf2536..d66b47a2d68 100644
--- a/nixos/modules/services/misc/ntfy-sh.nix
+++ b/nixos/modules/services/misc/ntfy-sh.nix
@@ -59,6 +59,10 @@ in
         systemPackages = [ cfg.package ];
       };
 
+      services.ntfy-sh.settings = {
+        auth-file = mkDefault "/var/lib/ntfy-sh/user.db";
+      };
+
       systemd.services.ntfy-sh = {
         description = "Push notifications server";
 
@@ -68,6 +72,7 @@ in
         serviceConfig = {
           ExecStart = "${cfg.package}/bin/ntfy serve -c ${configuration}";
           User = cfg.user;
+          StateDirectory = "ntfy-sh";
 
           AmbientCapabilities = "CAP_NET_BIND_SERVICE";
           PrivateTmp = true;
diff --git a/nixos/modules/services/misc/octoprint.nix b/nixos/modules/services/misc/octoprint.nix
index c216c6fa2b7..43e0ce0c21d 100644
--- a/nixos/modules/services/misc/octoprint.nix
+++ b/nixos/modules/services/misc/octoprint.nix
@@ -106,6 +106,9 @@ in
 
     systemd.tmpfiles.rules = [
       "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -"
+      # this will allow octoprint access to raspberry specific hardware to check for throttling
+      # read-only will not work: "VCHI initialization failed" error
+      "a /dev/vchiq - - - - u:octoprint:rw"
     ];
 
     systemd.services.octoprint = {
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index 6a98d5cb686..4199e771330 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -6,6 +6,7 @@ let
   pkg = cfg.package;
 
   defaultUser = "paperless";
+  nltkDir = "/var/cache/paperless/nltk";
 
   # Don't start a redis instance if the user sets a custom redis connection
   enableRedis = !hasAttr "PAPERLESS_REDIS" cfg.extraConfig;
@@ -15,6 +16,7 @@ let
     PAPERLESS_DATA_DIR = cfg.dataDir;
     PAPERLESS_MEDIA_ROOT = cfg.mediaDir;
     PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
+    PAPERLESS_NLTK_DIR = nltkDir;
     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
   } // optionalAttrs (config.time.timeZone != null) {
     PAPERLESS_TIME_ZONE = config.time.timeZone;
@@ -24,12 +26,14 @@ let
     lib.mapAttrs (_: toString) cfg.extraConfig
   );
 
-  manage = let
-    setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
-  in pkgs.writeShellScript "manage" ''
-    ${setupEnv}
-    exec ${pkg}/bin/paperless-ngx "$@"
-  '';
+  manage =
+    let
+      setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env);
+    in
+    pkgs.writeShellScript "manage" ''
+      ${setupEnv}
+      exec ${pkg}/bin/paperless-ngx "$@"
+    '';
 
   # Secure the services
   defaultServiceConfig = {
@@ -47,6 +51,7 @@ let
       cfg.dataDir
       cfg.mediaDir
     ];
+    CacheDirectory = "paperless";
     CapabilityBoundingSet = "";
     # ProtectClock adds DeviceAllow=char-rtc r
     DeviceAllow = "";
@@ -170,7 +175,7 @@ in
 
     extraConfig = mkOption {
       type = types.attrs;
-      default = {};
+      default = { };
       description = lib.mdDoc ''
         Extra paperless config options.
 
@@ -211,28 +216,41 @@ in
     ];
 
     systemd.services.paperless-scheduler = {
-      description = "Paperless scheduler";
+      description = "Paperless Celery Beat";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "paperless-consumer.service" "paperless-web.service" "paperless-task-queue.service" ];
       serviceConfig = defaultServiceConfig // {
         User = cfg.user;
-        ExecStart = "${pkg}/bin/paperless-ngx qcluster";
+        ExecStart = "${pkg}/bin/celery --app paperless beat --loglevel INFO";
         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-consumer.service" "paperless-web.service" ];
 
       preStart = ''
         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) != ${pkg} ]]; then
+        version=$(cat "$versionFile" 2>/dev/null || echo 0)
+
+        if [[ $version != ${pkg.version} ]]; then
           ${pkg}/bin/paperless-ngx migrate
-          echo ${pkg} > "$versionFile"
+
+          # Parse old version string format for backwards compatibility
+          version=$(echo "$version" | grep -ohP '[^-]+$')
+
+          versionLessThan() {
+            target=$1
+            [[ $({ echo "$version"; echo "$target"; } | sort -V | head -1) != "$target" ]]
+          }
+
+          if versionLessThan 1.12.0; then
+            # Reindex documents as mentioned in https://github.com/paperless-ngx/paperless-ngx/releases/tag/v1.12.1
+            echo "Reindexing documents, to allow searching old comments. Required after the 1.12.x upgrade."
+            ${pkg}/bin/paperless-ngx document_index reindex
+          fi
+
+          echo ${pkg.version} > "$versionFile"
         fi
       ''
       + optionalString (cfg.passwordFile != null) ''
@@ -250,6 +268,21 @@ in
       after = [ "redis-paperless.service" ];
     };
 
+    systemd.services.paperless-task-queue = {
+      description = "Paperless Celery Workers";
+      after = [ "paperless-scheduler.service" ];
+      serviceConfig = defaultServiceConfig // {
+        User = cfg.user;
+        ExecStart = "${pkg}/bin/celery --app paperless worker --loglevel INFO";
+        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;
+    };
+
     # Reading the user-provided password file requires root access
     systemd.services.paperless-copy-password = mkIf (cfg.passwordFile != null) {
       requiredBy = [ "paperless-scheduler.service" ];
@@ -263,22 +296,53 @@ in
       };
     };
 
+    # Download NLTK corpus data
+    systemd.services.paperless-download-nltk-data = {
+      wantedBy = [ "paperless-scheduler.service" ];
+      before = [ "paperless-scheduler.service" ];
+      after = [ "network-online.target" ];
+      serviceConfig = defaultServiceConfig // {
+        User = cfg.user;
+        Type = "oneshot";
+        # Enable internet access
+        PrivateNetwork = false;
+        # Restrict write access
+        BindPaths = [];
+        BindReadOnlyPaths = [
+          "/nix/store"
+          "-/etc/resolv.conf"
+          "-/etc/nsswitch.conf"
+          "-/etc/ssl/certs"
+          "-/etc/static/ssl/certs"
+          "-/etc/hosts"
+          "-/etc/localtime"
+        ];
+        ExecStart = let pythonWithNltk = pkg.python.withPackages (ps: [ ps.nltk ]); in ''
+          ${pythonWithNltk}/bin/python -m nltk.downloader -d '${nltkDir}' punkt snowball_data stopwords
+        '';
+      };
+    };
+
     systemd.services.paperless-consumer = {
       description = "Paperless document consumer";
+      # Bind to `paperless-scheduler` so that the consumer never runs
+      # during migrations
+      bindsTo = [ "paperless-scheduler.service" ];
+      after = [ "paperless-scheduler.service" ];
       serviceConfig = defaultServiceConfig // {
         User = cfg.user;
         ExecStart = "${pkg}/bin/paperless-ngx document_consumer";
         Restart = "on-failure";
       };
       environment = env;
-      # Bind to `paperless-scheduler` so that the consumer never runs
-      # during migrations
-      bindsTo = [ "paperless-scheduler.service" ];
-      after = [ "paperless-scheduler.service" ];
     };
 
     systemd.services.paperless-web = {
       description = "Paperless web server";
+      # Bind to `paperless-scheduler` so that the web server never runs
+      # during migrations
+      bindsTo = [ "paperless-scheduler.service" ];
+      after = [ "paperless-scheduler.service" ];
       serviceConfig = defaultServiceConfig // {
         User = cfg.user;
         ExecStart = ''
@@ -301,11 +365,7 @@ in
       };
       # 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-scheduler.service";
-      # Bind to `paperless-scheduler` so that the web server never runs
-      # during migrations
-      bindsTo = [ "paperless-scheduler.service" ];
-      after = [ "paperless-scheduler.service" ];
+      unitConfig.JoinsNamespaceOf = "paperless-task-queue.service";
     };
 
     users = optionalAttrs (cfg.user == defaultUser) {
diff --git a/nixos/modules/services/misc/polaris.nix b/nixos/modules/services/misc/polaris.nix
index 83da486083b..70f097f0284 100644
--- a/nixos/modules/services/misc/polaris.nix
+++ b/nixos/modules/services/misc/polaris.nix
@@ -13,7 +13,7 @@ in
     services.polaris = {
       enable = mkEnableOption (lib.mdDoc "Polaris Music Server");
 
-      package = mkPackageOption pkgs "polaris" { };
+      package = mkPackageOptionMD pkgs "polaris" { };
 
       user = mkOption {
         type = types.str;
diff --git a/nixos/modules/services/misc/portunus.nix b/nixos/modules/services/misc/portunus.nix
index f60cbe34771..5504fb94296 100644
--- a/nixos/modules/services/misc/portunus.nix
+++ b/nixos/modules/services/misc/portunus.nix
@@ -238,7 +238,7 @@ in
           PORTUNUS_SERVER_BINARY = "${cfg.package}/bin/portunus-server";
           PORTUNUS_SERVER_GROUP = cfg.group;
           PORTUNUS_SERVER_USER = cfg.user;
-          PORTUNUS_SERVER_HTTP_LISTEN = "[::]:${toString cfg.port}";
+          PORTUNUS_SERVER_HTTP_LISTEN = "127.0.0.1:${toString cfg.port}";
           PORTUNUS_SERVER_STATE_DIR = cfg.stateDir;
           PORTUNUS_SLAPD_BINARY = "${cfg.ldap.package}/libexec/slapd";
           PORTUNUS_SLAPD_GROUP = cfg.ldap.group;
diff --git a/nixos/modules/services/misc/pufferpanel.nix b/nixos/modules/services/misc/pufferpanel.nix
new file mode 100644
index 00000000000..78ec3564690
--- /dev/null
+++ b/nixos/modules/services/misc/pufferpanel.nix
@@ -0,0 +1,176 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.pufferpanel;
+in
+{
+  options.services.pufferpanel = {
+    enable = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to enable PufferPanel game management server.
+
+        Note that [PufferPanel templates] and binaries downloaded by PufferPanel
+        expect [FHS environment]. It is possible to set {option}`package` option
+        to use PufferPanel wrapper with FHS environment. For example, to use
+        `Download Game from Steam` and `Download Java` template operations:
+        ```Nix
+        { lib, pkgs, ... }: {
+          services.pufferpanel = {
+            enable = true;
+            extraPackages = with pkgs; [ bash curl gawk gnutar gzip ];
+            package = pkgs.buildFHSUserEnv {
+              name = "pufferpanel-fhs";
+              runScript = lib.getExe pkgs.pufferpanel;
+              targetPkgs = pkgs': with pkgs'; [ icu openssl zlib ];
+            };
+          };
+        }
+        ```
+
+        [PufferPanel templates]: https://github.com/PufferPanel/templates
+        [FHS environment]: https://wikipedia.org/wiki/Filesystem_Hierarchy_Standard
+      '';
+    };
+
+    package = lib.mkPackageOptionMD pkgs "pufferpanel" { };
+
+    extraGroups = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
+      default = [ ];
+      example = [ "podman" ];
+      description = lib.mdDoc ''
+        Additional groups for the systemd service.
+      '';
+    };
+
+    extraPackages = lib.mkOption {
+      type = lib.types.listOf lib.types.package;
+      default = [ ];
+      example = lib.literalExpression "[ pkgs.jre ]";
+      description = lib.mdDoc ''
+        Packages to add to the PATH environment variable. Both the {file}`bin`
+        and {file}`sbin` subdirectories of each package are added.
+      '';
+    };
+
+    environment = lib.mkOption {
+      type = lib.types.attrsOf lib.types.str;
+      default = { };
+      example = lib.literalExpression ''
+        {
+          PUFFER_WEB_HOST = ":8080";
+          PUFFER_DAEMON_SFTP_HOST = ":5657";
+          PUFFER_DAEMON_CONSOLE_BUFFER = "1000";
+          PUFFER_DAEMON_CONSOLE_FORWARD = "true";
+          PUFFER_PANEL_REGISTRATIONENABLED = "false";
+        }
+      '';
+      description = lib.mdDoc ''
+        Environment variables to set for the service. Secrets should be
+        specified using {option}`environmentFile`.
+
+        Refer to the [PufferPanel source code][] for the list of available
+        configuration options. Variable name is an upper-cased configuration
+        entry name with underscores instead of dots, prefixed with `PUFFER_`.
+        For example, `panel.settings.companyName` entry can be set using
+        {env}`PUFFER_PANEL_SETTINGS_COMPANYNAME`.
+
+        When running with panel enabled (configured with `PUFFER_PANEL_ENABLE`
+        environment variable), it is recommended disable registration using
+        `PUFFER_PANEL_REGISTRATIONENABLED` environment variable (registration is
+        enabled by default). To create the initial administrator user, run
+        {command}`pufferpanel --workDir /var/lib/pufferpanel user add --admin`.
+
+        Some options override corresponding settings set via web interface (e.g.
+        `PUFFER_PANEL_REGISTRATIONENABLED`). Those options can be temporarily
+        toggled or set in settings but do not persist between restarts.
+
+        [PufferPanel source code]: https://github.com/PufferPanel/PufferPanel/blob/master/config/entries.go
+      '';
+    };
+
+    environmentFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = lib.mdDoc ''
+        File to load environment variables from. Loaded variables override
+        values set in {option}`environment`.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.pufferpanel = {
+      description = "PufferPanel game management server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      path = cfg.extraPackages;
+      environment = cfg.environment;
+
+      # Note that we export environment variables for service directories if the
+      # value is not set. An empty environment variable is considered to be set.
+      # E.g.
+      #   export PUFFER_LOGS=${PUFFER_LOGS-$LOGS_DIRECTORY}
+      # would set PUFFER_LOGS to $LOGS_DIRECTORY if PUFFER_LOGS environment
+      # variable is not defined.
+      script = ''
+        ${lib.concatLines (lib.mapAttrsToList (name: value: ''
+          export ${name}="''${${name}-${value}}"
+        '') {
+          PUFFER_LOGS = "$LOGS_DIRECTORY";
+          PUFFER_DAEMON_DATA_CACHE = "$CACHE_DIRECTORY";
+          PUFFER_DAEMON_DATA_SERVERS = "$STATE_DIRECTORY/servers";
+          PUFFER_DAEMON_DATA_BINARIES = "$STATE_DIRECTORY/binaries";
+        })}
+        exec ${lib.getExe cfg.package} run --workDir "$STATE_DIRECTORY"
+      '';
+
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+
+        UMask = "0077";
+
+        SupplementaryGroups = cfg.extraGroups;
+
+        StateDirectory = "pufferpanel";
+        StateDirectoryMode = "0700";
+        CacheDirectory = "pufferpanel";
+        CacheDirectoryMode = "0700";
+        LogsDirectory = "pufferpanel";
+        LogsDirectoryMode = "0700";
+
+        EnvironmentFile = cfg.environmentFile;
+
+        # Command "pufferpanel shutdown --pid $MAINPID" sends SIGTERM (code 15)
+        # to the main process and waits for termination. This is essentially
+        # KillMode=mixed we are using here. See
+        # https://freedesktop.org/software/systemd/man/systemd.kill.html#KillMode=
+        KillMode = "mixed";
+
+        DynamicUser = true;
+        ProtectHome = true;
+        ProtectProc = "invisible";
+        ProtectClock = true;
+        ProtectHostname = true;
+        ProtectControlGroups = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        PrivateUsers = true;
+        PrivateDevices = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = [ "user" "mnt" ]; # allow buildFHSUserEnv
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+        LockPersonality = true;
+        DeviceAllow = [ "" ];
+        DevicePolicy = "closed";
+        CapabilityBoundingSet = [ "" ];
+      };
+    };
+  };
+
+  meta.maintainers = [ lib.maintainers.tie ];
+}
diff --git a/nixos/modules/services/misc/pykms.nix b/nixos/modules/services/misc/pykms.nix
index 314388e0152..be3accc0d7e 100644
--- a/nixos/modules/services/misc/pykms.nix
+++ b/nixos/modules/services/misc/pykms.nix
@@ -85,7 +85,7 @@ in
         WorkingDirectory = libDir;
         SyslogIdentifier = "pykms";
         Restart = "on-failure";
-        MemoryLimit = cfg.memoryLimit;
+        MemoryMax = cfg.memoryLimit;
       };
     };
   };
diff --git a/nixos/modules/services/misc/readarr.nix b/nixos/modules/services/misc/readarr.nix
new file mode 100644
index 00000000000..dd4fef6e598
--- /dev/null
+++ b/nixos/modules/services/misc/readarr.nix
@@ -0,0 +1,88 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.readarr;
+in
+{
+  options = {
+    services.readarr = {
+      enable = mkEnableOption (lib.mdDoc "Readarr");
+
+      dataDir = mkOption {
+        type = types.str;
+        default = "/var/lib/readarr/";
+        description = lib.mdDoc "The directory where Readarr stores its data files.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.readarr;
+        defaultText = literalExpression "pkgs.readarr";
+        description = lib.mdDoc "The Readarr package to use";
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Open ports in the firewall for Readarr
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "readarr";
+        description = lib.mdDoc ''
+          User account under which Readarr runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "readarr";
+        description = lib.mdDoc ''
+          Group under which Readarr runs.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}' 0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.readarr = {
+      description = "Readarr";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${cfg.package}/bin/Readarr -nobrowser -data='${cfg.dataDir}'";
+        Restart = "on-failure";
+      };
+    };
+
+    networking.firewall = mkIf cfg.openFirewall {
+      allowedTCPPorts = [ 8787 ];
+    };
+
+    users.users = mkIf (cfg.user == "readarr") {
+      readarr = {
+        description = "Readarr service";
+        home = cfg.dataDir;
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = mkIf (cfg.group == "readarr") {
+      readarr = { };
+    };
+  };
+}
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 58a595b5c76..d881ea91369 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -283,13 +283,13 @@ in
 
     services.redmine.settings = {
       production = {
-        scm_subversion_command = if cfg.components.subversion then "${pkgs.subversion}/bin/svn" else "";
-        scm_mercurial_command = if cfg.components.mercurial then "${pkgs.mercurial}/bin/hg" else "";
-        scm_git_command = if cfg.components.git then "${pkgs.git}/bin/git" else "";
-        scm_cvs_command = if cfg.components.cvs then "${pkgs.cvs}/bin/cvs" else "";
-        scm_bazaar_command = if cfg.components.breezy then "${pkgs.breezy}/bin/bzr" else "";
-        imagemagick_convert_command = if cfg.components.imagemagick then "${pkgs.imagemagick}/bin/convert" else "";
-        gs_command = if cfg.components.ghostscript then "${pkgs.ghostscript}/bin/gs" else "";
+        scm_subversion_command = optionalString cfg.components.subversion "${pkgs.subversion}/bin/svn";
+        scm_mercurial_command = optionalString cfg.components.mercurial "${pkgs.mercurial}/bin/hg";
+        scm_git_command = optionalString cfg.components.git "${pkgs.git}/bin/git";
+        scm_cvs_command = optionalString cfg.components.cvs "${pkgs.cvs}/bin/cvs";
+        scm_bazaar_command = optionalString cfg.components.breezy "${pkgs.breezy}/bin/bzr";
+        imagemagick_convert_command = optionalString cfg.components.imagemagick "${pkgs.imagemagick}/bin/convert";
+        gs_command = optionalString cfg.components.ghostscript "${pkgs.ghostscript}/bin/gs";
         minimagick_font_path = "${cfg.components.minimagick_font_path}";
       };
     };
diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix
index f1a1ed4d29b..99b25bdb8e9 100644
--- a/nixos/modules/services/misc/siproxd.nix
+++ b/nixos/modules/services/misc/siproxd.nix
@@ -20,7 +20,7 @@ let
     ${optionalString (cfg.hostsAllowReg != []) "hosts_allow_reg = ${concatStringsSep "," cfg.hostsAllowReg}"}
     ${optionalString (cfg.hostsAllowSip != []) "hosts_allow_sip = ${concatStringsSep "," cfg.hostsAllowSip}"}
     ${optionalString (cfg.hostsDenySip != []) "hosts_deny_sip  = ${concatStringsSep "," cfg.hostsDenySip}"}
-    ${if (cfg.passwordFile != "") then "proxy_auth_pwfile = ${cfg.passwordFile}" else ""}
+    ${optionalString (cfg.passwordFile != "") "proxy_auth_pwfile = ${cfg.passwordFile}"}
     ${cfg.extraConfig}
   '';
 
diff --git a/nixos/modules/services/misc/snapper.nix b/nixos/modules/services/misc/snapper.nix
index cfdfa2830ce..30ed30e7a8f 100644
--- a/nixos/modules/services/misc/snapper.nix
+++ b/nixos/modules/services/misc/snapper.nix
@@ -175,7 +175,7 @@ in
       description = "Take snapper snapshot of root on boot";
       inherit documentation;
       serviceConfig.ExecStart = "${pkgs.snapper}/bin/snapper --config root create --cleanup-algorithm number --description boot";
-      serviceConfig.type = "oneshot";
+      serviceConfig.Type = "oneshot";
       requires = [ "local-fs.target" ];
       wantedBy = [ "multi-user.target" ];
       unitConfig.ConditionPathExists = "/etc/snapper/configs/root";
diff --git a/nixos/modules/services/misc/sourcehut/default.md b/nixos/modules/services/misc/sourcehut/default.md
new file mode 100644
index 00000000000..44d58aa0bef
--- /dev/null
+++ b/nixos/modules/services/misc/sourcehut/default.md
@@ -0,0 +1,93 @@
+# Sourcehut {#module-services-sourcehut}
+
+[Sourcehut](https://sr.ht.com/) is an open-source,
+self-hostable software development platform. The server setup can be automated using
+[services.sourcehut](#opt-services.sourcehut.enable).
+
+## Basic usage {#module-services-sourcehut-basic-usage}
+
+Sourcehut is a Python and Go based set of applications.
+This NixOS module also provides basic configuration integrating Sourcehut into locally running
+`services.nginx`, `services.redis.servers.sourcehut`, `services.postfix`
+and `services.postgresql` services.
+
+A very basic configuration may look like this:
+```
+{ pkgs, ... }:
+let
+  fqdn =
+    let
+      join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
+    in join config.networking.hostName config.networking.domain;
+in {
+
+  networking = {
+    hostName = "srht";
+    domain = "tld";
+    firewall.allowedTCPPorts = [ 22 80 443 ];
+  };
+
+  services.sourcehut = {
+    enable = true;
+    git.enable = true;
+    man.enable = true;
+    meta.enable = true;
+    nginx.enable = true;
+    postfix.enable = true;
+    postgresql.enable = true;
+    redis.enable = true;
+    settings = {
+        "sr.ht" = {
+          environment = "production";
+          global-domain = fqdn;
+          origin = "https://${fqdn}";
+          # Produce keys with srht-keygen from sourcehut.coresrht.
+          network-key = "/run/keys/path/to/network-key";
+          service-key = "/run/keys/path/to/service-key";
+        };
+        webhooks.private-key= "/run/keys/path/to/webhook-key";
+    };
+  };
+
+  security.acme.certs."${fqdn}".extraDomainNames = [
+    "meta.${fqdn}"
+    "man.${fqdn}"
+    "git.${fqdn}"
+  ];
+
+  services.nginx = {
+    enable = true;
+    # only recommendedProxySettings are strictly required, but the rest make sense as well.
+    recommendedTlsSettings = true;
+    recommendedOptimisation = true;
+    recommendedGzipSettings = true;
+    recommendedProxySettings = true;
+
+    # Settings to setup what certificates are used for which endpoint.
+    virtualHosts = {
+      "${fqdn}".enableACME = true;
+      "meta.${fqdn}".useACMEHost = fqdn:
+      "man.${fqdn}".useACMEHost = fqdn:
+      "git.${fqdn}".useACMEHost = fqdn:
+    };
+  };
+}
+```
+
+  The `hostName` option is used internally to configure the nginx
+reverse-proxy. The `settings` attribute set is
+used by the configuration generator and the result is placed in `/etc/sr.ht/config.ini`.
+
+## Configuration {#module-services-sourcehut-configuration}
+
+All configuration parameters are also stored in
+`/etc/sr.ht/config.ini` which is generated by
+the module and linked from the store to ensure that all values from `config.ini`
+can be modified by the module.
+
+## Using an alternative webserver as reverse-proxy (e.g. `httpd`) {#module-services-sourcehut-httpd}
+
+By default, `nginx` is used as reverse-proxy for `sourcehut`.
+However, it's possible to use e.g. `httpd` by explicitly disabling
+`nginx` using [](#opt-services.nginx.enable) and fixing the
+`settings`.
diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix
index 7dd254e3492..d4391bc49e3 100644
--- a/nixos/modules/services/misc/sourcehut/default.nix
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -438,7 +438,7 @@ in
         };
 
         options."lists.sr.ht" = commonServiceSettings "lists" // {
-          allow-new-lists = mkEnableOption (lib.mdDoc "Allow creation of new lists.");
+          allow-new-lists = mkEnableOption (lib.mdDoc "Allow creation of new lists");
           notify-from = mkOption {
             description = lib.mdDoc "Outgoing email for notifications generated by users.";
             type = types.str;
@@ -1390,6 +1390,6 @@ in
     '')
   ];
 
-  meta.doc = ./sourcehut.xml;
+  meta.doc = ./default.md;
   meta.maintainers = with maintainers; [ tomberek ];
 }
diff --git a/nixos/modules/services/misc/sourcehut/sourcehut.xml b/nixos/modules/services/misc/sourcehut/sourcehut.xml
deleted file mode 100644
index 41094f65a94..00000000000
--- a/nixos/modules/services/misc/sourcehut/sourcehut.xml
+++ /dev/null
@@ -1,119 +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-sourcehut">
- <title>Sourcehut</title>
- <para>
-  <link xlink:href="https://sr.ht.com/">Sourcehut</link> is an open-source,
-  self-hostable software development platform. The server setup can be automated using
-  <link linkend="opt-services.sourcehut.enable">services.sourcehut</link>.
- </para>
-
- <section xml:id="module-services-sourcehut-basic-usage">
-  <title>Basic usage</title>
-  <para>
-   Sourcehut is a Python and Go based set of applications.
-   This NixOS module also provides basic configuration integrating Sourcehut into locally running
-   <literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>,
-   <literal><link linkend="opt-services.redis.servers">services.redis.servers.sourcehut</link></literal>,
-   <literal><link linkend="opt-services.postfix.enable">services.postfix</link></literal>
-   and
-   <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal> services.
-  </para>
-
-  <para>
-   A very basic configuration may look like this:
-<programlisting>
-{ pkgs, ... }:
-let
-  fqdn =
-    let
-      join = hostName: domain: hostName + optionalString (domain != null) ".${domain}";
-    in join config.networking.hostName config.networking.domain;
-in {
-
-  networking = {
-    <link linkend="opt-networking.hostName">hostName</link> = "srht";
-    <link linkend="opt-networking.domain">domain</link> = "tld";
-    <link linkend="opt-networking.firewall.allowedTCPPorts">firewall.allowedTCPPorts</link> = [ 22 80 443 ];
-  };
-
-  services.sourcehut = {
-    <link linkend="opt-services.sourcehut.enable">enable</link> = true;
-    <link linkend="opt-services.sourcehut.git.enable">git.enable</link> = true;
-    <link linkend="opt-services.sourcehut.man.enable">man.enable</link> = true;
-    <link linkend="opt-services.sourcehut.meta.enable">meta.enable</link> = true;
-    <link linkend="opt-services.sourcehut.nginx.enable">nginx.enable</link> = true;
-    <link linkend="opt-services.sourcehut.postfix.enable">postfix.enable</link> = true;
-    <link linkend="opt-services.sourcehut.postgresql.enable">postgresql.enable</link> = true;
-    <link linkend="opt-services.sourcehut.redis.enable">redis.enable</link> = true;
-    <link linkend="opt-services.sourcehut.settings">settings</link> = {
-        "sr.ht" = {
-          environment = "production";
-          global-domain = fqdn;
-          origin = "https://${fqdn}";
-          # Produce keys with srht-keygen from <package>sourcehut.coresrht</package>.
-          network-key = "/run/keys/path/to/network-key";
-          service-key = "/run/keys/path/to/service-key";
-        };
-        webhooks.private-key= "/run/keys/path/to/webhook-key";
-    };
-  };
-
-  <link linkend="opt-security.acme.certs._name_.extraDomainNames">security.acme.certs."${fqdn}".extraDomainNames</link> = [
-    "meta.${fqdn}"
-    "man.${fqdn}"
-    "git.${fqdn}"
-  ];
-
-  services.nginx = {
-    <link linkend="opt-services.nginx.enable">enable</link> = true;
-    # only recommendedProxySettings 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;
-
-    # Settings to setup what certificates are used for which endpoint.
-    <link linkend="opt-services.nginx.virtualHosts">virtualHosts</link> = {
-      <link linkend="opt-services.nginx.virtualHosts._name_.enableACME">"${fqdn}".enableACME</link> = true;
-      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"meta.${fqdn}".useACMEHost</link> = fqdn:
-      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"man.${fqdn}".useACMEHost</link> = fqdn:
-      <link linkend="opt-services.nginx.virtualHosts._name_.useACMEHost">"git.${fqdn}".useACMEHost</link> = fqdn:
-    };
-  };
-}
-</programlisting>
-  </para>
-
-  <para>
-   The <literal>hostName</literal> option is used internally to configure the nginx
-   reverse-proxy. The <literal>settings</literal> attribute set is
-   used by the configuration generator and the result is placed in <literal>/etc/sr.ht/config.ini</literal>.
-  </para>
- </section>
-
- <section xml:id="module-services-sourcehut-configuration">
-  <title>Configuration</title>
-
-  <para>
-   All configuration parameters are also stored in
-   <literal>/etc/sr.ht/config.ini</literal> which is generated by
-   the module and linked from the store to ensure that all values from <literal>config.ini</literal>
-   can be modified by the module.
-  </para>
-
- </section>
-
- <section xml:id="module-services-sourcehut-httpd">
-  <title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title>
-  <para>
-   By default, <package>nginx</package> is used as reverse-proxy for <package>sourcehut</package>.
-   However, it's possible to use e.g. <package>httpd</package> by explicitly disabling
-   <package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the
-   <literal>settings</literal>.
-  </para>
-</section>
-
-</chapter>
diff --git a/nixos/modules/services/misc/sssd.nix b/nixos/modules/services/misc/sssd.nix
index edd5750a4a4..7c7a3b464a8 100644
--- a/nixos/modules/services/misc/sssd.nix
+++ b/nixos/modules/services/misc/sssd.nix
@@ -77,6 +77,10 @@ in {
   };
   config = mkMerge [
     (mkIf cfg.enable {
+      # For `sssctl` to work.
+      environment.etc."sssd/sssd.conf".source = settingsFile;
+      environment.etc."sssd/conf.d".source = "${dataDir}/conf.d";
+
       systemd.services.sssd = {
         description = "System Security Services Daemon";
         wantedBy    = [ "multi-user.target" ];
@@ -101,6 +105,7 @@ in {
           EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
         };
         preStart = ''
+          mkdir -p "${dataDir}/conf.d"
           [ -f ${settingsFile} ] && rm -f ${settingsFile}
           old_umask=$(umask)
           umask 0177
diff --git a/nixos/modules/services/misc/tandoor-recipes.nix b/nixos/modules/services/misc/tandoor-recipes.nix
index a349bcac932..63d3e3d2a85 100644
--- a/nixos/modules/services/misc/tandoor-recipes.nix
+++ b/nixos/modules/services/misc/tandoor-recipes.nix
@@ -9,6 +9,7 @@ let
   env = {
     GUNICORN_CMD_ARGS = "--bind=${cfg.address}:${toString cfg.port}";
     DEBUG = "0";
+    DEBUG_TOOLBAR = "0";
     MEDIA_ROOT = "/var/lib/tandoor-recipes";
   } // optionalAttrs (config.time.timeZone != null) {
     TIMEZONE = config.time.timeZone;
diff --git a/nixos/modules/services/misc/taskserver/default.md b/nixos/modules/services/misc/taskserver/default.md
new file mode 100644
index 00000000000..ee3b3908e2a
--- /dev/null
+++ b/nixos/modules/services/misc/taskserver/default.md
@@ -0,0 +1,93 @@
+# Taskserver {#module-services-taskserver}
+
+Taskserver is the server component of
+[Taskwarrior](https://taskwarrior.org/), a free and
+open source todo list application.
+
+*Upstream documentation:* <https://taskwarrior.org/docs/#taskd>
+
+## Configuration {#module-services-taskserver-configuration}
+
+Taskserver does all of its authentication via TLS using client certificates,
+so you either need to roll your own CA or purchase a certificate from a
+known CA, which allows creation of client certificates. These certificates
+are usually advertised as "server certificates".
+
+So in order to make it easier to handle your own CA, there is a helper tool
+called {command}`nixos-taskserver` which manages the custom CA along
+with Taskserver organisations, users and groups.
+
+While the client certificates in Taskserver only authenticate whether a user
+is allowed to connect, every user has its own UUID which identifies it as an
+entity.
+
+With {command}`nixos-taskserver` the client certificate is created
+along with the UUID of the user, so it handles all of the credentials needed
+in order to setup the Taskwarrior client to work with a Taskserver.
+
+## The nixos-taskserver tool {#module-services-taskserver-nixos-taskserver-tool}
+
+Because Taskserver by default only provides scripts to setup users
+imperatively, the {command}`nixos-taskserver` tool is used for
+addition and deletion of organisations along with users and groups defined
+by [](#opt-services.taskserver.organisations) and as well for
+imperative set up.
+
+The tool is designed to not interfere if the command is used to manually set
+up some organisations, users or groups.
+
+For example if you add a new organisation using {command}`nixos-taskserver
+org add foo`, the organisation is not modified and deleted no
+matter what you define in
+{option}`services.taskserver.organisations`, even if you're adding
+the same organisation in that option.
+
+The tool is modelled to imitate the official {command}`taskd`
+command, documentation for each subcommand can be shown by using the
+{option}`--help` switch.
+
+## Declarative/automatic CA management {#module-services-taskserver-declarative-ca-management}
+
+Everything is done according to what you specify in the module options,
+however in order to set up a Taskwarrior client for synchronisation with a
+Taskserver instance, you have to transfer the keys and certificates to the
+client machine.
+
+This is done using {command}`nixos-taskserver user export $orgname
+$username` which is printing a shell script fragment to stdout
+which can either be used verbatim or adjusted to import the user on the
+client machine.
+
+For example, let's say you have the following configuration:
+```ShellSession
+{
+  services.taskserver.enable = true;
+  services.taskserver.fqdn = "server";
+  services.taskserver.listenHost = "::";
+  services.taskserver.organisations.my-company.users = [ "alice" ];
+}
+```
+This creates an organisation called `my-company` with the
+user `alice`.
+
+Now in order to import the `alice` user to another machine
+`alicebox`, all we need to do is something like this:
+```ShellSession
+$ ssh server nixos-taskserver user export my-company alice | sh
+```
+Of course, if no SSH daemon is available on the server you can also copy
+&amp; paste it directly into a shell.
+
+After this step the user should be set up and you can start synchronising
+your tasks for the first time with {command}`task sync init` on
+`alicebox`.
+
+Subsequent synchronisation requests merely require the command {command}`task
+sync` after that stage.
+
+## Manual CA management {#module-services-taskserver-manual-ca-management}
+
+If you set any options within
+[service.taskserver.pki.manual](#opt-services.taskserver.pki.manual.ca.cert).*,
+{command}`nixos-taskserver` won't issue certificates, but you can
+still use it for adding or removing user accounts.
diff --git a/nixos/modules/services/misc/taskserver/default.nix b/nixos/modules/services/misc/taskserver/default.nix
index ee4bf42183f..775b3b6d2ea 100644
--- a/nixos/modules/services/misc/taskserver/default.nix
+++ b/nixos/modules/services/misc/taskserver/default.nix
@@ -566,5 +566,5 @@ in {
     })
   ];
 
-  meta.doc = ./doc.xml;
+  meta.doc = ./default.md;
 }
diff --git a/nixos/modules/services/misc/taskserver/doc.xml b/nixos/modules/services/misc/taskserver/doc.xml
deleted file mode 100644
index f6ead7c3785..00000000000
--- a/nixos/modules/services/misc/taskserver/doc.xml
+++ /dev/null
@@ -1,135 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-    xmlns:xlink="http://www.w3.org/1999/xlink"
-    version="5.0"
-    xml:id="module-services-taskserver">
- <title>Taskserver</title>
- <para>
-  Taskserver is the server component of
-  <link xlink:href="https://taskwarrior.org/">Taskwarrior</link>, a free and
-  open source todo list application.
- </para>
- <para>
-  <emphasis>Upstream documentation:</emphasis>
-  <link xlink:href="https://taskwarrior.org/docs/#taskd"/>
- </para>
- <section xml:id="module-services-taskserver-configuration">
-  <title>Configuration</title>
-
-  <para>
-   Taskserver does all of its authentication via TLS using client certificates,
-   so you either need to roll your own CA or purchase a certificate from a
-   known CA, which allows creation of client certificates. These certificates
-   are usually advertised as <quote>server certificates</quote>.
-  </para>
-
-  <para>
-   So in order to make it easier to handle your own CA, there is a helper tool
-   called <command>nixos-taskserver</command> which manages the custom CA along
-   with Taskserver organisations, users and groups.
-  </para>
-
-  <para>
-   While the client certificates in Taskserver only authenticate whether a user
-   is allowed to connect, every user has its own UUID which identifies it as an
-   entity.
-  </para>
-
-  <para>
-   With <command>nixos-taskserver</command> the client certificate is created
-   along with the UUID of the user, so it handles all of the credentials needed
-   in order to setup the Taskwarrior client to work with a Taskserver.
-  </para>
- </section>
- <section xml:id="module-services-taskserver-nixos-taskserver-tool">
-  <title>The nixos-taskserver tool</title>
-
-  <para>
-   Because Taskserver by default only provides scripts to setup users
-   imperatively, the <command>nixos-taskserver</command> tool is used for
-   addition and deletion of organisations along with users and groups defined
-   by <xref linkend="opt-services.taskserver.organisations"/> and as well for
-   imperative set up.
-  </para>
-
-  <para>
-   The tool is designed to not interfere if the command is used to manually set
-   up some organisations, users or groups.
-  </para>
-
-  <para>
-   For example if you add a new organisation using <command>nixos-taskserver
-   org add foo</command>, the organisation is not modified and deleted no
-   matter what you define in
-   <option>services.taskserver.organisations</option>, even if you're adding
-   the same organisation in that option.
-  </para>
-
-  <para>
-   The tool is modelled to imitate the official <command>taskd</command>
-   command, documentation for each subcommand can be shown by using the
-   <option>--help</option> switch.
-  </para>
- </section>
- <section xml:id="module-services-taskserver-declarative-ca-management">
-  <title>Declarative/automatic CA management</title>
-
-  <para>
-   Everything is done according to what you specify in the module options,
-   however in order to set up a Taskwarrior client for synchronisation with a
-   Taskserver instance, you have to transfer the keys and certificates to the
-   client machine.
-  </para>
-
-  <para>
-   This is done using <command>nixos-taskserver user export $orgname
-   $username</command> which is printing a shell script fragment to stdout
-   which can either be used verbatim or adjusted to import the user on the
-   client machine.
-  </para>
-
-  <para>
-   For example, let's say you have the following configuration:
-<screen>
-{
-  <xref linkend="opt-services.taskserver.enable"/> = true;
-  <xref linkend="opt-services.taskserver.fqdn"/> = "server";
-  <xref linkend="opt-services.taskserver.listenHost"/> = "::";
-  <link linkend="opt-services.taskserver.organisations._name_.users">services.taskserver.organisations.my-company.users</link> = [ "alice" ];
-}
-</screen>
-   This creates an organisation called <literal>my-company</literal> with the
-   user <literal>alice</literal>.
-  </para>
-
-  <para>
-   Now in order to import the <literal>alice</literal> user to another machine
-   <literal>alicebox</literal>, all we need to do is something like this:
-<screen>
-<prompt>$ </prompt>ssh server nixos-taskserver user export my-company alice | sh
-</screen>
-   Of course, if no SSH daemon is available on the server you can also copy
-   &amp; paste it directly into a shell.
-  </para>
-
-  <para>
-   After this step the user should be set up and you can start synchronising
-   your tasks for the first time with <command>task sync init</command> on
-   <literal>alicebox</literal>.
-  </para>
-
-  <para>
-   Subsequent synchronisation requests merely require the command <command>task
-   sync</command> after that stage.
-  </para>
- </section>
- <section xml:id="module-services-taskserver-manual-ca-management">
-  <title>Manual CA management</title>
-
-  <para>
-   If you set any options within
-   <link linkend="opt-services.taskserver.pki.manual.ca.cert">service.taskserver.pki.manual</link>.*,
-   <command>nixos-taskserver</command> won't issue certificates, but you can
-   still use it for adding or removing user accounts.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/misc/weechat.md b/nixos/modules/services/misc/weechat.md
new file mode 100644
index 00000000000..21f41be5b4a
--- /dev/null
+++ b/nixos/modules/services/misc/weechat.md
@@ -0,0 +1,46 @@
+# WeeChat {#module-services-weechat}
+
+[WeeChat](https://weechat.org/) is a fast and
+extensible IRC client.
+
+## Basic Usage {#module-services-weechat-basic-usage}
+
+By default, the module creates a
+[`systemd`](https://www.freedesktop.org/wiki/Software/systemd/)
+unit which runs the chat client in a detached
+[`screen`](https://www.gnu.org/software/screen/)
+session.
+
+This can be done by enabling the `weechat` service:
+```
+{ ... }:
+
+{
+  services.weechat.enable = true;
+}
+```
+
+The service is managed by a dedicated user named `weechat`
+in the state directory `/var/lib/weechat`.
+
+## Re-attaching to WeeChat {#module-services-weechat-reattach}
+
+WeeChat runs in a screen session owned by a dedicated user. To explicitly
+allow your another user to attach to this session, the
+`screenrc` needs to be tweaked by adding
+[multiuser](https://www.gnu.org/software/screen/manual/html_node/Multiuser.html#Multiuser)
+support:
+```
+{
+  programs.screen.screenrc = ''
+    multiuser on
+    acladd normal_user
+  '';
+}
+```
+Now, the session can be re-attached like this:
+```
+screen -x weechat/weechat-screen
+```
+
+*The session name can be changed using [services.weechat.sessionName.](options.html#opt-services.weechat.sessionName)*
diff --git a/nixos/modules/services/misc/weechat.nix b/nixos/modules/services/misc/weechat.nix
index 663a767a0c1..338493e3cd3 100644
--- a/nixos/modules/services/misc/weechat.nix
+++ b/nixos/modules/services/misc/weechat.nix
@@ -15,7 +15,7 @@ in
       default = "/var/lib/weechat";
     };
     sessionName = mkOption {
-      description = lib.mdDoc "Name of the `screen' session for weechat.";
+      description = lib.mdDoc "Name of the `screen` session for weechat.";
       default = "weechat-screen";
       type = types.str;
     };
@@ -59,5 +59,5 @@ in
       };
   };
 
-  meta.doc = ./weechat.xml;
+  meta.doc = ./weechat.md;
 }
diff --git a/nixos/modules/services/misc/weechat.xml b/nixos/modules/services/misc/weechat.xml
deleted file mode 100644
index 7255edfb9da..00000000000
--- a/nixos/modules/services/misc/weechat.xml
+++ /dev/null
@@ -1,66 +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-weechat">
- <title>WeeChat</title>
- <para>
-  <link xlink:href="https://weechat.org/">WeeChat</link> is a fast and
-  extensible IRC client.
- </para>
- <section xml:id="module-services-weechat-basic-usage">
-  <title>Basic Usage</title>
-
-  <para>
-   By default, the module creates a
-   <literal><link xlink:href="https://www.freedesktop.org/wiki/Software/systemd/">systemd</link></literal>
-   unit which runs the chat client in a detached
-   <literal><link xlink:href="https://www.gnu.org/software/screen/">screen</link></literal>
-   session.
-  </para>
-
-  <para>
-   This can be done by enabling the <literal>weechat</literal> service:
-<programlisting>
-{ ... }:
-
-{
-  <link linkend="opt-services.weechat.enable">services.weechat.enable</link> = true;
-}
-</programlisting>
-  </para>
-
-  <para>
-   The service is managed by a dedicated user named <literal>weechat</literal>
-   in the state directory <literal>/var/lib/weechat</literal>.
-  </para>
- </section>
- <section xml:id="module-services-weechat-reattach">
-  <title>Re-attaching to WeeChat</title>
-
-  <para>
-   WeeChat runs in a screen session owned by a dedicated user. To explicitly
-   allow your another user to attach to this session, the
-   <literal>screenrc</literal> needs to be tweaked by adding
-   <link xlink:href="https://www.gnu.org/software/screen/manual/html_node/Multiuser.html#Multiuser">multiuser</link>
-   support:
-<programlisting>
-{
-  <link linkend="opt-programs.screen.screenrc">programs.screen.screenrc</link> = ''
-    multiuser on
-    acladd normal_user
-  '';
-}
-</programlisting>
-   Now, the session can be re-attached like this:
-<programlisting>
-screen -x weechat/weechat-screen
-</programlisting>
-  </para>
-
-  <para>
-   <emphasis>The session name can be changed using
-   <link linkend="opt-services.weechat.sessionName">services.weechat.sessionName.</link></emphasis>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/misc/zoneminder.nix b/nixos/modules/services/misc/zoneminder.nix
index 109415a20ee..11722979851 100644
--- a/nixos/modules/services/misc/zoneminder.nix
+++ b/nixos/modules/services/misc/zoneminder.nix
@@ -283,7 +283,8 @@ in {
       phpfpm = lib.mkIf useNginx {
         pools.zoneminder = {
           inherit user group;
-          phpPackage = pkgs.php.withExtensions ({ enabled, all }: enabled ++ [ all.apcu ]);
+          phpPackage = pkgs.php.withExtensions (
+            { enabled, all }: enabled ++ [ all.apcu all.sysvsem ]);
           phpOptions = ''
             date.timezone = "${config.time.timeZone}"
           '';
@@ -326,6 +327,15 @@ in {
           fi
 
           ${zoneminder}/bin/zmupdate.pl -nointeractive
+          ${zoneminder}/bin/zmupdate.pl --nointeractive -f
+
+          # Update ZM's Nix store path in the configuration table. Do nothing if the config doesn't
+          # contain ZM's Nix store path.
+          ${config.services.mysql.package}/bin/mysql -u zoneminder zm << EOF
+            UPDATE Config
+              SET Value = REGEXP_REPLACE(Value, "^/nix/store/[^-/]+-zoneminder-[^/]+", "${pkgs.zoneminder}")
+              WHERE Name = "ZM_FONT_FILE_LOCATION";
+          EOF
         '';
         serviceConfig = {
           User = user;
diff --git a/nixos/modules/services/monitoring/apcupsd.nix b/nixos/modules/services/monitoring/apcupsd.nix
index d4216b44cdc..666479c78a8 100644
--- a/nixos/modules/services/monitoring/apcupsd.nix
+++ b/nixos/modules/services/monitoring/apcupsd.nix
@@ -62,6 +62,21 @@ let
 
   );
 
+  # Ensure the CLI uses our generated configFile
+  wrappedBinaries = pkgs.runCommandLocal "apcupsd-wrapped-binaries"
+    { nativeBuildInputs = [ pkgs.makeWrapper ]; }
+    ''
+      for p in "${lib.getBin pkgs.apcupsd}/bin/"*; do
+          bname=$(basename "$p")
+          makeWrapper "$p" "$out/bin/$bname" --add-flags "-f ${configFile}"
+      done
+    '';
+
+  apcupsdWrapped = pkgs.symlinkJoin {
+    name = "apcupsd-wrapped";
+    # Put wrappers first so they "win"
+    paths = [ wrappedBinaries pkgs.apcupsd ];
+  };
 in
 
 {
@@ -138,7 +153,7 @@ in
     } ];
 
     # Give users access to the "apcaccess" tool
-    environment.systemPackages = [ pkgs.apcupsd ];
+    environment.systemPackages = [ apcupsdWrapped ];
 
     # NOTE 1: apcupsd runs as root because it needs permission to run
     # "shutdown"
diff --git a/nixos/modules/services/monitoring/cadvisor.nix b/nixos/modules/services/monitoring/cadvisor.nix
index a8fba4e6e8c..68e6e8e40b3 100644
--- a/nixos/modules/services/monitoring/cadvisor.nix
+++ b/nixos/modules/services/monitoring/cadvisor.nix
@@ -123,7 +123,7 @@ in {
             ${escapeShellArgs cfg.extraOptions} \
             ${optionalString (cfg.storageDriver != null) ''
               -storage_driver "${cfg.storageDriver}" \
-              -storage_driver_user "${cfg.storageDriverHost}" \
+              -storage_driver_host "${cfg.storageDriverHost}" \
               -storage_driver_db "${cfg.storageDriverDb}" \
               -storage_driver_user "${cfg.storageDriverUser}" \
               -storage_driver_password "$(cat "${cfg.storageDriverPasswordFile}")" \
diff --git a/nixos/modules/services/monitoring/cockpit.nix b/nixos/modules/services/monitoring/cockpit.nix
new file mode 100644
index 00000000000..2947b4d8012
--- /dev/null
+++ b/nixos/modules/services/monitoring/cockpit.nix
@@ -0,0 +1,231 @@
+{ pkgs, config, lib, ... }:
+
+let
+  cfg = config.services.cockpit;
+  inherit (lib) types mkEnableOption mkOption mkIf mdDoc literalMD mkPackageOptionMD;
+  settingsFormat = pkgs.formats.ini {};
+in {
+  options = {
+    services.cockpit = {
+      enable = mkEnableOption (mdDoc "Cockpit");
+
+      package = mkPackageOptionMD pkgs "Cockpit" {
+        default = [ "cockpit" ];
+      };
+
+      settings = lib.mkOption {
+        type = settingsFormat.type;
+
+        default = {};
+
+        description = mdDoc ''
+          Settings for cockpit that will be saved in /etc/cockpit/cockpit.conf.
+
+          See the [documentation](https://cockpit-project.org/guide/latest/cockpit.conf.5.html), that is also available with `man cockpit.conf.5` for details.
+        '';
+      };
+
+      port = mkOption {
+        description = mdDoc "Port where cockpit will listen.";
+        type = types.port;
+        default = 9090;
+      };
+
+      openFirewall = mkOption {
+        description = mdDoc "Open port for cockpit.";
+        type = types.bool;
+        default = false;
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+
+    # expose cockpit-bridge system-wide
+    environment.systemPackages = [ cfg.package ];
+
+    # allow cockpit to find its plugins
+    environment.pathsToLink = [ "/share/cockpit" ];
+
+    # generate cockpit settings
+    environment.etc."cockpit/cockpit.conf".source = settingsFormat.generate "cockpit.conf" cfg.settings;
+
+    security.pam.services.cockpit = {};
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+    # units are in reverse sort order if you ls $out/lib/systemd/system
+    # all these units are basically verbatim translated from upstream
+
+    # Translation from $out/lib/systemd/system/systemd-cockpithttps.slice
+    systemd.slices.system-cockpithttps = {
+      description = "Resource limits for all cockpit-ws-https@.service instances";
+      sliceConfig = {
+        TasksMax = 200;
+        MemoryHigh = "75%";
+        MemoryMax = "90%";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-https@.socket
+    systemd.sockets."cockpit-wsinstance-https@" = {
+      unitConfig = {
+        Description = "Socket for Cockpit Web Service https instance %I";
+        BindsTo = [ "cockpit.service" "cockpit-wsinstance-https@%i.service" ];
+        # clean up the socket after the service exits, to prevent fd leak
+        # this also effectively prevents a DoS by starting arbitrarily many sockets, as
+        # the services are resource-limited by system-cockpithttps.slice
+        Documentation = "man:cockpit-ws(8)";
+      };
+      socketConfig = {
+        ListenStream = "/run/cockpit/wsinstance/https@%i.sock";
+        SocketUser = "root";
+        SocketMode = "0600";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-https@.service
+    systemd.services."cockpit-wsinstance-https@" = {
+      description = "Cockpit Web Service https instance %I";
+      bindsTo = [ "cockpit.service"];
+      path = [ cfg.package ];
+      documentation = [ "man:cockpit-ws(8)" ];
+      serviceConfig = {
+        Slice = "system-cockpithttps.slice";
+        ExecStart = "${cfg.package}/libexec/cockpit-ws --for-tls-proxy --port=0";
+        User = "root";
+        Group = "";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-http.socket
+    systemd.sockets.cockpit-wsinstance-http = {
+      unitConfig = {
+        Description = "Socket for Cockpit Web Service http instance";
+        BindsTo = "cockpit.service";
+        Documentation = "man:cockpit-ws(8)";
+      };
+      socketConfig = {
+        ListenStream = "/run/cockpit/wsinstance/http.sock";
+        SocketUser = "root";
+        SocketMode = "0600";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-https-factory.socket
+    systemd.sockets.cockpit-wsinstance-https-factory = {
+      unitConfig = {
+        Description = "Socket for Cockpit Web Service https instance factory";
+        BindsTo = "cockpit.service";
+        Documentation = "man:cockpit-ws(8)";
+      };
+      socketConfig = {
+        ListenStream = "/run/cockpit/wsinstance/https-factory.sock";
+        Accept = true;
+        SocketUser = "root";
+        SocketMode = "0600";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-https-factory@.service
+    systemd.services."cockpit-wsinstance-https-factory@" = {
+      description = "Cockpit Web Service https instance factory";
+      documentation = [ "man:cockpit-ws(8)" ];
+      path = [ cfg.package ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/libexec/cockpit-wsinstance-factory";
+        User = "root";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-wsinstance-http.service
+    systemd.services."cockpit-wsinstance-http" = {
+      description = "Cockpit Web Service http instance";
+      bindsTo = [ "cockpit.service" ];
+      path = [ cfg.package ];
+      documentation = [ "man:cockpit-ws(8)" ];
+      serviceConfig = {
+        ExecStart = "${cfg.package}/libexec/cockpit-ws --no-tls --port=0";
+        User = "root";
+        Group = "";
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit.socket
+    systemd.sockets."cockpit" = {
+      unitConfig = {
+        Description = "Cockpit Web Service Socket";
+        Documentation = "man:cockpit-ws(8)";
+        Wants = "cockpit-motd.service";
+      };
+      socketConfig = {
+        ListenStream = cfg.port;
+        ExecStartPost = [
+          "-${cfg.package}/share/cockpit/motd/update-motd \"\" localhost"
+          "-${pkgs.coreutils}/bin/ln -snf active.motd /run/cockpit/motd"
+        ];
+        ExecStopPost = "-${pkgs.coreutils}/bin/ln -snf inactive.motd /run/cockpit/motd";
+      };
+      wantedBy = [ "sockets.target" ];
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit.service
+    systemd.services."cockpit" = {
+      description = "Cockpit Web Service";
+      documentation = [ "man:cockpit-ws(8)" ];
+      restartIfChanged = true;
+      path = with pkgs; [ coreutils cfg.package ];
+      requires = [ "cockpit.socket" "cockpit-wsinstance-http.socket" "cockpit-wsinstance-https-factory.socket" ];
+      after = [ "cockpit-wsinstance-http.socket" "cockpit-wsinstance-https-factory.socket" ];
+      environment = {
+        G_MESSAGES_DEBUG = "cockpit-ws,cockpit-bridge";
+      };
+      serviceConfig = {
+        RuntimeDirectory="cockpit/tls";
+        ExecStartPre = [
+          # cockpit-tls runs in a more constrained environment, these + means that these commands
+          # will run with full privilege instead of inside that constrained environment
+          # See https://www.freedesktop.org/software/systemd/man/systemd.service.html#ExecStart= for details
+          "+${cfg.package}/libexec/cockpit-certificate-ensure --for-cockpit-tls"
+        ];
+        ExecStart = "${cfg.package}/libexec/cockpit-tls";
+        User = "root";
+        Group = "";
+        NoNewPrivileges = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        MemoryDenyWriteExecute = true;
+      };
+    };
+
+    # Translation from $out/lib/systemd/system/cockpit-motd.service
+    # This part basically implements a motd state machine:
+    # - If cockpit.socket is enabled then /run/cockpit/motd points to /run/cockpit/active.motd
+    # - If cockpit.socket is disabled then /run/cockpit/motd points to /run/cockpit/inactive.motd
+    # - As cockpit.socket is disabled by default, /run/cockpit/motd points to /run/cockpit/inactive.motd
+    # /run/cockpit/active.motd is generated dynamically by cockpit-motd.service
+    systemd.services."cockpit-motd" = {
+      path = with pkgs; [ nettools ];
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${cfg.package}/share/cockpit/motd/update-motd";
+      };
+      description = "Cockpit motd updater service";
+      documentation = [ "man:cockpit-ws(8)" ];
+      wants = [ "network.target" ];
+      after = [ "network.target" "cockpit.socket" ];
+    };
+
+    systemd.tmpfiles.rules = [ # From $out/lib/tmpfiles.d/cockpit-tmpfiles.conf
+      "C /run/cockpit/inactive.motd 0640 root root - ${cfg.package}/share/cockpit/motd/inactive.motd"
+      "f /run/cockpit/active.motd   0640 root root -"
+      "L+ /run/cockpit/motd - - - - inactive.motd"
+      "d /etc/cockpit/ws-certs.d 0600 root root 0"
+    ];
+  };
+
+  meta.maintainers = pkgs.cockpit.meta.maintainers;
+}
diff --git a/nixos/modules/services/monitoring/datadog-agent.nix b/nixos/modules/services/monitoring/datadog-agent.nix
index 15deef18b60..58a0faed962 100644
--- a/nixos/modules/services/monitoring/datadog-agent.nix
+++ b/nixos/modules/services/monitoring/datadog-agent.nix
@@ -235,7 +235,7 @@ in {
 
     systemd.services = let
       makeService = attrs: recursiveUpdate {
-        path = [ datadogPkg pkgs.python pkgs.sysstat pkgs.procps pkgs.iproute2 ];
+        path = [ datadogPkg pkgs.sysstat pkgs.procps pkgs.iproute2 ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
           User = "datadog";
diff --git a/nixos/modules/services/monitoring/grafana-agent.nix b/nixos/modules/services/monitoring/grafana-agent.nix
index 270d888afb7..a462e760f74 100644
--- a/nixos/modules/services/monitoring/grafana-agent.nix
+++ b/nixos/modules/services/monitoring/grafana-agent.nix
@@ -13,12 +13,7 @@ in
   options.services.grafana-agent = {
     enable = mkEnableOption (lib.mdDoc "grafana-agent");
 
-    package = mkOption {
-      type = types.package;
-      default = pkgs.grafana-agent;
-      defaultText = lib.literalExpression "pkgs.grafana-agent";
-      description = lib.mdDoc "The grafana-agent package to use.";
-    };
+    package = mkPackageOptionMD pkgs "grafana-agent" { };
 
     credentials = mkOption {
       description = lib.mdDoc ''
@@ -37,11 +32,22 @@ in
       };
     };
 
+    extraFlags = mkOption {
+      type = with types; listOf str;
+      default = [ ];
+      example = [ "-enable-features=integrations-next" "-disable-reporting" ];
+      description = lib.mdDoc ''
+        Extra command-line flags passed to {command}`grafana-agent`.
+
+        See <https://grafana.com/docs/agent/latest/static/configuration/flags/>
+      '';
+    };
+
     settings = mkOption {
       description = lib.mdDoc ''
-        Configuration for `grafana-agent`.
+        Configuration for {command}`grafana-agent`.
 
-        See https://grafana.com/docs/agent/latest/configuration/
+        See <https://grafana.com/docs/agent/latest/configuration/>
       '';
 
       type = types.submodule {
@@ -140,7 +146,7 @@ in
         # 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}
+        exec ${lib.getExe cfg.package} -config.expand-env -config.file ${configFile} ${escapeShellArgs cfg.extraFlags}
       '';
       serviceConfig = {
         Restart = "always";
diff --git a/nixos/modules/services/monitoring/grafana-image-renderer.nix b/nixos/modules/services/monitoring/grafana-image-renderer.nix
index 60f6e84c63c..36258866646 100644
--- a/nixos/modules/services/monitoring/grafana-image-renderer.nix
+++ b/nixos/modules/services/monitoring/grafana-image-renderer.nix
@@ -107,8 +107,8 @@ in {
     ];
 
     services.grafana.settings.rendering = mkIf cfg.provisionGrafana {
-      url = "http://localhost:${toString cfg.settings.service.port}/render";
-      callback_url = "http://localhost:${toString config.services.grafana.port}";
+      server_url = "http://localhost:${toString cfg.settings.service.port}/render";
+      callback_url = "http://localhost:${toString config.services.grafana.settings.server.http_port}";
     };
 
     services.grafana-image-renderer.chromium = mkDefault pkgs.chromium;
diff --git a/nixos/modules/services/monitoring/grafana.nix b/nixos/modules/services/monitoring/grafana.nix
index 9a9a0ab7553..e74ee641db3 100644
--- a/nixos/modules/services/monitoring/grafana.nix
+++ b/nixos/modules/services/monitoring/grafana.nix
@@ -56,7 +56,7 @@ let
   provisionConfDir = pkgs.runCommand "grafana-provisioning" { nativeBuildInputs = [ pkgs.xorg.lndir ]; } ''
     mkdir -p $out/{datasources,dashboards,notifiers,alerting}
     ${ln { src = datasourceFileOrDir;    dir = "datasources"; filename = "datasource"; }}
-    ${ln { src = dashboardFileOrDir;     dir = "dashboards";  filename = "dashbaord"; }}
+    ${ln { src = dashboardFileOrDir;     dir = "dashboards";  filename = "dashboard"; }}
     ${ln { src = notifierFileOrDir;      dir = "notifiers";   filename = "notifier"; }}
     ${ln { src = rulesFileOrDir;         dir = "alerting";    filename = "rules"; }}
     ${ln { src = contactPointsFileOrDir; dir = "alerting";    filename = "contactPoints"; }}
@@ -92,17 +92,6 @@ let
   grafanaTypes.datasourceConfig = types.submodule {
     freeformType = provisioningSettingsFormat.type;
 
-    imports = [
-      (mkRemovedOptionModule [ "password" ] ''
-        `services.grafana.provision.datasources.settings.datasources.<name>.password` has been removed
-        in Grafana 9. Use `secureJsonData` instead.
-      '')
-      (mkRemovedOptionModule [ "basicAuthPassword" ] ''
-        `services.grafana.provision.datasources.settings.datasources.<name>.basicAuthPassword` has been removed
-        in Grafana 9. Use `secureJsonData` instead.
-      '')
-    ];
-
     options = {
       name = mkOption {
         type = types.str;
@@ -364,9 +353,15 @@ in {
             };
 
             http_addr = mkOption {
-              description = lib.mdDoc "Listening address.";
-              default = "";
               type = types.str;
+              default = "127.0.0.1";
+              description = lib.mdDoc ''
+                Listening address.
+
+                ::: {.note}
+                This setting intentionally varies from upstream's default to be a bit more secure by default.
+                :::
+              '';
             };
 
             http_port = mkOption {
@@ -597,7 +592,6 @@ in {
                   description = lib.mdDoc "List of datasources to insert/update.";
                   default = [];
                   type = types.listOf grafanaTypes.datasourceConfig;
-                  apply = map (flip builtins.removeAttrs [ "password" "basicAuthPassword" ]);
                 };
 
                 deleteDatasources = mkOption {
@@ -1294,7 +1288,7 @@ in {
         SystemCallFilter = [
           "@system-service"
           "~@privileged"
-        ] ++ lib.optional (cfg.settings.server.protocol == "socket") [ "@chown" ];
+        ] ++ lib.optionals (cfg.settings.server.protocol == "socket") [ "@chown" ];
         UMask = "0027";
       };
       preStart = ''
diff --git a/nixos/modules/services/monitoring/loki.nix b/nixos/modules/services/monitoring/loki.nix
index 11bb8497c9b..f3b97e9151e 100644
--- a/nixos/modules/services/monitoring/loki.nix
+++ b/nixos/modules/services/monitoring/loki.nix
@@ -22,6 +22,8 @@ in {
       '';
     };
 
+    package = lib.mkPackageOptionMD pkgs "grafana-loki" { };
+
     group = mkOption {
       type = types.str;
       default = "loki";
@@ -78,7 +80,7 @@ in {
       '';
     }];
 
-    environment.systemPackages = [ pkgs.grafana-loki ]; # logcli
+    environment.systemPackages = [ cfg.package ]; # logcli
 
     users.groups.${cfg.group} = { };
     users.users.${cfg.user} = {
@@ -99,7 +101,7 @@ in {
                else cfg.configFile;
       in
       {
-        ExecStart = "${pkgs.grafana-loki}/bin/loki --config.file=${conf} ${escapeShellArgs cfg.extraFlags}";
+        ExecStart = "${cfg.package}/bin/loki --config.file=${conf} ${escapeShellArgs cfg.extraFlags}";
         User = cfg.user;
         Restart = "always";
         PrivateTmp = true;
diff --git a/nixos/modules/services/monitoring/mackerel-agent.nix b/nixos/modules/services/monitoring/mackerel-agent.nix
index 4185cd76c4e..67dc1bc19ed 100644
--- a/nixos/modules/services/monitoring/mackerel-agent.nix
+++ b/nixos/modules/services/monitoring/mackerel-agent.nix
@@ -11,7 +11,7 @@ in {
 
     # the upstream package runs as root, but doesn't seem to be strictly
     # necessary for basic functionality
-    runAsRoot = mkEnableOption (lib.mdDoc "Whether to run as root.");
+    runAsRoot = mkEnableOption (lib.mdDoc "Whether to run as root");
 
     autoRetirement = mkEnableOption (lib.mdDoc ''
       Whether to automatically retire the host upon OS shutdown.
diff --git a/nixos/modules/services/monitoring/mimir.nix b/nixos/modules/services/monitoring/mimir.nix
index 568066990f2..edca9b7be4f 100644
--- a/nixos/modules/services/monitoring/mimir.nix
+++ b/nixos/modules/services/monitoring/mimir.nix
@@ -25,6 +25,13 @@ in {
         Specify a configuration file that Mimir should use.
       '';
     };
+
+    package = mkOption {
+      default = pkgs.mimir;
+      defaultText = lib.literalExpression "pkgs.mimir";
+      type = types.package;
+      description = lib.mdDoc ''Mimir package to use.'';
+    };
   };
 
   config = mkIf cfg.enable {
@@ -53,7 +60,7 @@ in {
                else cfg.configFile;
       in
       {
-        ExecStart = "${pkgs.mimir}/bin/mimir --config.file=${conf}";
+        ExecStart = "${cfg.package}/bin/mimir --config.file=${conf}";
         DynamicUser = true;
         Restart = "always";
         ProtectSystem = "full";
diff --git a/nixos/modules/services/monitoring/netdata.nix b/nixos/modules/services/monitoring/netdata.nix
index 92c870bb23f..bd0dea83e1a 100644
--- a/nixos/modules/services/monitoring/netdata.nix
+++ b/nixos/modules/services/monitoring/netdata.nix
@@ -169,6 +169,20 @@ in {
           See: <https://learn.netdata.cloud/docs/agent/anonymous-statistics>
         '';
       };
+
+      deadlineBeforeStopSec = mkOption {
+        type = types.int;
+        default = 120;
+        description = lib.mdDoc ''
+          In order to detect when netdata is misbehaving, we run a concurrent task pinging netdata (wait-for-netdata-up)
+          in the systemd unit.
+
+          If after a while, this task does not succeed, we stop the unit and mark it as failed.
+
+          You can control this deadline in seconds with this option, it's useful to bump it
+          if you have (1) a lot of data (2) doing upgrades (3) have low IOPS/throughput.
+        '';
+      };
     };
   };
 
@@ -205,7 +219,7 @@ in {
           while [ "$(${pkgs.netdata}/bin/netdatacli ping)" != pong ]; do sleep 0.5; done
         '';
 
-        TimeoutStopSec = 60;
+        TimeoutStopSec = cfg.deadlineBeforeStopSec;
         Restart = "on-failure";
         # User and group
         User = cfg.user;
diff --git a/nixos/modules/services/monitoring/parsedmarc.md b/nixos/modules/services/monitoring/parsedmarc.md
index d93134a4cc7..eac07e0cc9f 100644
--- a/nixos/modules/services/monitoring/parsedmarc.md
+++ b/nixos/modules/services/monitoring/parsedmarc.md
@@ -17,7 +17,6 @@ services.parsedmarc = {
     host = "imap.example.com";
     user = "alice@example.com";
     password = "/path/to/imap_password_file";
-    watch = true;
   };
   provision.geoIp = false; # Not recommended!
 };
@@ -26,7 +25,7 @@ services.parsedmarc = {
 Note that GeoIP provisioning is disabled in the example for
 simplicity, but should be turned on for fully functional reports.
 
-## Local mail
+## Local mail {#module-services-parsedmarc-local-mail}
 Instead of watching an external inbox, a local inbox can be
 automatically provisioned. The recipient's name is by default set to
 `dmarc`, but can be configured in
@@ -50,7 +49,7 @@ services.parsedmarc = {
 };
 ```
 
-## Grafana and GeoIP
+## Grafana and GeoIP {#module-services-parsedmarc-grafana-geoip}
 The reports can be visualized and summarized with parsedmarc's
 official Grafana dashboard. For all views to work, and for the data to
 be complete, GeoIP databases are also required. The following example
diff --git a/nixos/modules/services/monitoring/parsedmarc.nix b/nixos/modules/services/monitoring/parsedmarc.nix
index 3540d91fc9f..44fc359b6a7 100644
--- a/nixos/modules/services/monitoring/parsedmarc.nix
+++ b/nixos/modules/services/monitoring/parsedmarc.nix
@@ -123,7 +123,10 @@ in
             host = "imap.example.com";
             user = "alice@example.com";
             password = { _secret = "/run/keys/imap_password" };
+          };
+          mailbox = {
             watch = true;
+            batch_size = 30;
           };
           splunk_hec = {
             url = "https://splunkhec.example.com";
@@ -170,6 +173,24 @@ in
             };
           };
 
+          mailbox = {
+            watch = lib.mkOption {
+              type = lib.types.bool;
+              default = true;
+              description = lib.mdDoc ''
+                Use the IMAP IDLE command to process messages as they arrive.
+              '';
+            };
+
+            delete = lib.mkOption {
+              type = lib.types.bool;
+              default = false;
+              description = lib.mdDoc ''
+                Delete messages after processing them, instead of archiving them.
+              '';
+            };
+          };
+
           imap = {
             host = lib.mkOption {
               type = lib.types.str;
@@ -216,22 +237,6 @@ in
               '';
               apply = x: if isAttrs x || x == null then x else { _secret = x; };
             };
-
-            watch = lib.mkOption {
-              type = lib.types.bool;
-              default = true;
-              description = lib.mdDoc ''
-                Use the IMAP IDLE command to process messages as they arrive.
-              '';
-            };
-
-            delete = lib.mkOption {
-              type = lib.types.bool;
-              default = false;
-              description = lib.mdDoc ''
-                Delete messages after processing them, instead of archiving them.
-              '';
-            };
           };
 
           smtp = {
@@ -360,6 +365,13 @@ in
 
   config = lib.mkIf cfg.enable {
 
+    warnings = let
+      deprecationWarning = optname: "Starting in 8.0.0, the `${optname}` option has been moved from the `services.parsedmarc.settings.imap`"
+        + "configuration section to the `services.parsedmarc.settings.mailbox` configuration section.";
+      hasImapOpt = lib.flip builtins.hasAttr cfg.settings.imap;
+      movedOptions = [ "reports_folder" "archive_folder" "watch" "delete" "test" "batch_size" ];
+    in builtins.map deprecationWarning (builtins.filter hasImapOpt movedOptions);
+
     services.elasticsearch.enable = lib.mkDefault cfg.provision.elasticsearch;
 
     services.geoipupdate = lib.mkIf cfg.provision.geoIp {
@@ -397,7 +409,7 @@ in
 
       provision = {
         enable = cfg.provision.grafana.datasource || cfg.provision.grafana.dashboard;
-        datasources =
+        datasources.settings.datasources =
           let
             esVersion = lib.getVersion config.services.elasticsearch.package;
           in
@@ -423,7 +435,7 @@ in
                 };
               }
             ];
-        dashboards = lib.mkIf cfg.provision.grafana.dashboard [{
+        dashboards.settings.providers = lib.mkIf cfg.provision.grafana.dashboard [{
           name = "parsedmarc";
           options.path = "${pkgs.python3Packages.parsedmarc.dashboard}";
         }];
@@ -444,6 +456,8 @@ in
           ssl = false;
           user = cfg.provision.localMail.recipientName;
           password = "${pkgs.writeText "imap-password" "@imap-password@"}";
+        };
+        mailbox = {
           watch = true;
         };
       })
@@ -525,8 +539,6 @@ in
     };
   };
 
-  # Don't edit the docbook xml directly, edit the md and generate it:
-  # `pandoc parsedmarc.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > parsedmarc.xml`
-  meta.doc = ./parsedmarc.xml;
+  meta.doc = ./parsedmarc.md;
   meta.maintainers = [ lib.maintainers.talyz ];
 }
diff --git a/nixos/modules/services/monitoring/parsedmarc.xml b/nixos/modules/services/monitoring/parsedmarc.xml
deleted file mode 100644
index 7167b52d035..00000000000
--- a/nixos/modules/services/monitoring/parsedmarc.xml
+++ /dev/null
@@ -1,125 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-parsedmarc">
-  <title>parsedmarc</title>
-  <para>
-    <link xlink:href="https://domainaware.github.io/parsedmarc/">parsedmarc</link>
-    is a service which parses incoming
-    <link xlink:href="https://dmarc.org/">DMARC</link> reports and
-    stores or sends them to a downstream service for further analysis.
-    In combination with Elasticsearch, Grafana and the included Grafana
-    dashboard, it provides a handy overview of DMARC reports over time.
-  </para>
-  <section xml:id="module-services-parsedmarc-basic-usage">
-    <title>Basic usage</title>
-    <para>
-      A very minimal setup which reads incoming reports from an external
-      email address and saves them to a local Elasticsearch instance
-      looks like this:
-    </para>
-    <programlisting language="bash">
-services.parsedmarc = {
-  enable = true;
-  settings.imap = {
-    host = &quot;imap.example.com&quot;;
-    user = &quot;alice@example.com&quot;;
-    password = &quot;/path/to/imap_password_file&quot;;
-    watch = true;
-  };
-  provision.geoIp = false; # Not recommended!
-};
-</programlisting>
-    <para>
-      Note that GeoIP provisioning is disabled in the example for
-      simplicity, but should be turned on for fully functional reports.
-    </para>
-  </section>
-  <section xml:id="local-mail">
-    <title>Local mail</title>
-    <para>
-      Instead of watching an external inbox, a local inbox can be
-      automatically provisioned. The recipient’s name is by default set
-      to <literal>dmarc</literal>, but can be configured in
-      <link xlink:href="options.html#opt-services.parsedmarc.provision.localMail.recipientName">services.parsedmarc.provision.localMail.recipientName</link>.
-      You need to add an MX record pointing to the host. More
-      concretely: for the example to work, an MX record needs to be set
-      up for <literal>monitoring.example.com</literal> and the complete
-      email address that should be configured in the domain’s dmarc
-      policy is <literal>dmarc@monitoring.example.com</literal>.
-    </para>
-    <programlisting language="bash">
-services.parsedmarc = {
-  enable = true;
-  provision = {
-    localMail = {
-      enable = true;
-      hostname = monitoring.example.com;
-    };
-    geoIp = false; # Not recommended!
-  };
-};
-</programlisting>
-  </section>
-  <section xml:id="grafana-and-geoip">
-    <title>Grafana and GeoIP</title>
-    <para>
-      The reports can be visualized and summarized with parsedmarc’s
-      official Grafana dashboard. For all views to work, and for the
-      data to be complete, GeoIP databases are also required. The
-      following example shows a basic deployment where the provisioned
-      Elasticsearch instance is automatically added as a Grafana
-      datasource, and the dashboard is added to Grafana as well.
-    </para>
-    <programlisting language="bash">
-services.parsedmarc = {
-  enable = true;
-  provision = {
-    localMail = {
-      enable = true;
-      hostname = url;
-    };
-    grafana = {
-      datasource = true;
-      dashboard = true;
-    };
-  };
-};
-
-# Not required, but recommended for full functionality
-services.geoipupdate = {
-  settings = {
-    AccountID = 000000;
-    LicenseKey = &quot;/path/to/license_key_file&quot;;
-  };
-};
-
-services.grafana = {
-  enable = true;
-  addr = &quot;0.0.0.0&quot;;
-  domain = url;
-  rootUrl = &quot;https://&quot; + url;
-  protocol = &quot;socket&quot;;
-  security = {
-    adminUser = &quot;admin&quot;;
-    adminPasswordFile = &quot;/path/to/admin_password_file&quot;;
-    secretKeyFile = &quot;/path/to/secret_key_file&quot;;
-  };
-};
-
-services.nginx = {
-  enable = true;
-  recommendedTlsSettings = true;
-  recommendedOptimisation = true;
-  recommendedGzipSettings = true;
-  recommendedProxySettings = true;
-  upstreams.grafana.servers.&quot;unix:/${config.services.grafana.socket}&quot; = {};
-  virtualHosts.${url} = {
-    root = config.services.grafana.staticRootPath;
-    enableACME = true;
-    forceSSL = true;
-    locations.&quot;/&quot;.tryFiles = &quot;$uri @grafana&quot;;
-    locations.&quot;@grafana&quot;.proxyPass = &quot;http://grafana&quot;;
-  };
-};
-users.users.nginx.extraGroups = [ &quot;grafana&quot; ];
-</programlisting>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix b/nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix
new file mode 100644
index 00000000000..b81d5f6db5e
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/alertmanager-irc-relay.nix
@@ -0,0 +1,107 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.alertmanagerIrcRelay;
+
+  configFormat = pkgs.formats.yaml { };
+  configFile = configFormat.generate "alertmanager-irc-relay.yml" cfg.settings;
+in
+{
+  options.services.prometheus.alertmanagerIrcRelay = {
+    enable = mkEnableOption (mdDoc "Alertmanager IRC Relay");
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.alertmanager-irc-relay;
+      defaultText = literalExpression "pkgs.alertmanager-irc-relay";
+      description = mdDoc "Alertmanager IRC Relay package to use.";
+    };
+
+    extraFlags = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = mdDoc "Extra command line options to pass to alertmanager-irc-relay.";
+    };
+
+    settings = mkOption {
+      type = configFormat.type;
+      example = literalExpression ''
+        {
+          http_host = "localhost";
+          http_port = 8000;
+
+          irc_host = "irc.example.com";
+          irc_port = 7000;
+          irc_nickname = "myalertbot";
+
+          irc_channels = [
+            { name = "#mychannel"; }
+          ];
+        }
+      '';
+      description = mdDoc ''
+        Configuration for Alertmanager IRC Relay as a Nix attribute set.
+        For a reference, check out the
+        [example configuration](https://github.com/google/alertmanager-irc-relay#configuring-and-running-the-bot)
+        and the
+        [source code](https://github.com/google/alertmanager-irc-relay/blob/master/config.go).
+
+        Note: The webhook's URL MUST point to the IRC channel where the message
+        should be posted. For `#mychannel` from the example, this would be
+        `http://localhost:8080/mychannel`.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.alertmanager-irc-relay = {
+      description = "Alertmanager IRC Relay";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+
+      serviceConfig = {
+        ExecStart = ''
+          ${cfg.package}/bin/alertmanager-irc-relay \
+          -config ${configFile} \
+          ${escapeShellArgs cfg.extraFlags}
+        '';
+
+        DynamicUser = true;
+        NoNewPrivileges = true;
+
+        ProtectProc = "invisible";
+        ProtectSystem = "strict";
+        ProtectHome = "tmpfs";
+
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateIPC = true;
+
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+
+        SystemCallFilter = [
+          "@system-service"
+          "~@cpu-emulation"
+          "~@privileged"
+          "~@reboot"
+          "~@setuid"
+          "~@swap"
+        ];
+      };
+    };
+  };
+
+  meta.maintainers = [ maintainers.oxzi ];
+}
diff --git a/nixos/modules/services/monitoring/prometheus/default.nix b/nixos/modules/services/monitoring/prometheus/default.nix
index f516b75ab10..fb3bab7963e 100644
--- a/nixos/modules/services/monitoring/prometheus/default.nix
+++ b/nixos/modules/services/monitoring/prometheus/default.nix
@@ -31,7 +31,7 @@ let
     if checkConfigEnabled then
       pkgs.runCommandLocal
         "${name}-${replaceStrings [" "] [""] what}-checked"
-        { buildInputs = [ cfg.package ]; } ''
+        { buildInputs = [ cfg.package.cli ]; } ''
         ln -s ${file} $out
         promtool ${what} $out
       '' else file;
@@ -1408,7 +1408,7 @@ let
       '';
 
       action =
-        mkDefOpt (types.enum [ "replace" "keep" "drop" "hashmod" "labelmap" "labeldrop" "labelkeep" ]) "replace" ''
+        mkDefOpt (types.enum [ "replace" "lowercase" "uppercase" "keep" "drop" "hashmod" "labelmap" "labeldrop" "labelkeep" ]) "replace" ''
           Action to perform based on regex matching.
         '';
     };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.md b/nixos/modules/services/monitoring/prometheus/exporters.md
new file mode 100644
index 00000000000..c085e46d20d
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters.md
@@ -0,0 +1,180 @@
+# Prometheus exporters {#module-services-prometheus-exporters}
+
+Prometheus exporters provide metrics for the
+[prometheus monitoring system](https://prometheus.io).
+
+## Configuration {#module-services-prometheus-exporters-configuration}
+
+One of the most common exporters is the
+[node exporter](https://github.com/prometheus/node_exporter),
+it provides hardware and OS metrics from the host it's
+running on. The exporter could be configured as follows:
+```
+  services.prometheus.exporters.node = {
+    enable = true;
+    port = 9100;
+    enabledCollectors = [
+      "logind"
+      "systemd"
+    ];
+    disabledCollectors = [
+      "textfile"
+    ];
+    openFirewall = true;
+    firewallFilter = "-i br0 -p tcp -m tcp --dport 9100";
+  };
+```
+It should now serve all metrics from the collectors that are explicitly
+enabled and the ones that are
+[enabled by default](https://github.com/prometheus/node_exporter#enabled-by-default),
+via http under `/metrics`. In this
+example the firewall should just allow incoming connections to the
+exporter's port on the bridge interface `br0` (this would
+have to be configured separately of course). For more information about
+configuration see `man configuration.nix` or search through
+the [available options](https://nixos.org/nixos/options.html#prometheus.exporters).
+
+Prometheus can now be configured to consume the metrics produced by the exporter:
+```
+    services.prometheus = {
+      # ...
+
+      scrapeConfigs = [
+        {
+          job_name = "node";
+          static_configs = [{
+            targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ];
+          }];
+        }
+      ];
+
+      # ...
+    }
+```
+
+## Adding a new exporter {#module-services-prometheus-exporters-new-exporter}
+
+To add a new exporter, it has to be packaged first (see
+`nixpkgs/pkgs/servers/monitoring/prometheus/` for
+examples), then a module can be added. The postfix exporter is used in this
+example:
+
+  - Some default options for all exporters are provided by
+    `nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.nix`:
+
+      - `enable`
+      - `port`
+      - `listenAddress`
+      - `extraFlags`
+      - `openFirewall`
+      - `firewallFilter`
+      - `user`
+      - `group`
+  - As there is already a package available, the module can now be added. This
+    is accomplished by adding a new file to the
+    `nixos/modules/services/monitoring/prometheus/exporters/`
+    directory, which will be called postfix.nix and contains all exporter
+    specific options and configuration:
+    ```
+    # nixpgs/nixos/modules/services/prometheus/exporters/postfix.nix
+    { config, lib, pkgs, options }:
+
+    with lib;
+
+    let
+      # for convenience we define cfg here
+      cfg = config.services.prometheus.exporters.postfix;
+    in
+    {
+      port = 9154; # The postfix exporter listens on this port by default
+
+      # `extraOpts` is an attribute set which contains additional options
+      # (and optional overrides for default options).
+      # Note that this attribute is optional.
+      extraOpts = {
+        telemetryPath = mkOption {
+          type = types.str;
+          default = "/metrics";
+          description = ''
+            Path under which to expose metrics.
+          '';
+        };
+        logfilePath = mkOption {
+          type = types.path;
+          default = /var/log/postfix_exporter_input.log;
+          example = /var/log/mail.log;
+          description = ''
+            Path where Postfix writes log entries.
+            This file will be truncated by this exporter!
+          '';
+        };
+        showqPath = mkOption {
+          type = types.path;
+          default = /var/spool/postfix/public/showq;
+          example = /var/lib/postfix/queue/public/showq;
+          description = ''
+            Path at which Postfix places its showq socket.
+          '';
+        };
+      };
+
+      # `serviceOpts` is an attribute set which contains configuration
+      # for the exporter's systemd service. One of
+      # `serviceOpts.script` and `serviceOpts.serviceConfig.ExecStart`
+      # has to be specified here. This will be merged with the default
+      # service configuration.
+      # Note that by default 'DynamicUser' is 'true'.
+      serviceOpts = {
+        serviceConfig = {
+          DynamicUser = false;
+          ExecStart = ''
+            ${pkgs.prometheus-postfix-exporter}/bin/postfix_exporter \
+              --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
+              --web.telemetry-path ${cfg.telemetryPath} \
+              ${concatStringsSep " \\\n  " cfg.extraFlags}
+          '';
+        };
+      };
+    }
+    ```
+  - This should already be enough for the postfix exporter. Additionally one
+    could now add assertions and conditional default values. This can be done
+    in the 'meta-module' that combines all exporter definitions and generates
+    the submodules:
+    `nixpkgs/nixos/modules/services/prometheus/exporters.nix`
+
+## Updating an exporter module {#module-services-prometheus-exporters-update-exporter-module}
+
+Should an exporter option change at some point, it is possible to add
+information about the change to the exporter definition similar to
+`nixpkgs/nixos/modules/rename.nix`:
+```
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.nginx;
+in
+{
+  port = 9113;
+  extraOpts = {
+    # additional module options
+    # ...
+  };
+  serviceOpts = {
+    # service configuration
+    # ...
+  };
+  imports = [
+    # 'services.prometheus.exporters.nginx.telemetryEndpoint' -> 'services.prometheus.exporters.nginx.telemetryPath'
+    (mkRenamedOptionModule [ "telemetryEndpoint" ] [ "telemetryPath" ])
+
+    # removed option 'services.prometheus.exporters.nginx.insecure'
+    (mkRemovedOptionModule [ "insecure" ] ''
+      This option was replaced by 'prometheus.exporters.nginx.sslVerify' which defaults to true.
+    '')
+    ({ options.warnings = options.warnings; })
+  ];
+}
+```
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.nix b/nixos/modules/services/monitoring/prometheus/exporters.nix
index f3fbfb149ad..fd40dce1410 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters.nix
@@ -64,6 +64,7 @@ let
     "rspamd"
     "rtl_433"
     "script"
+    "shelly"
     "snmp"
     "smartctl"
     "smokeping"
@@ -323,7 +324,7 @@ in
   );
 
   meta = {
-    doc = ./exporters.xml;
+    doc = ./exporters.md;
     maintainers = [ maintainers.willibutz ];
   };
 }
diff --git a/nixos/modules/services/monitoring/prometheus/exporters.xml b/nixos/modules/services/monitoring/prometheus/exporters.xml
deleted file mode 100644
index e922e1ace8d..00000000000
--- a/nixos/modules/services/monitoring/prometheus/exporters.xml
+++ /dev/null
@@ -1,248 +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-prometheus-exporters">
- <title>Prometheus exporters</title>
- <para>
-  Prometheus exporters provide metrics for the
-  <link xlink:href="https://prometheus.io">prometheus monitoring system</link>.
- </para>
- <section xml:id="module-services-prometheus-exporters-configuration">
-  <title>Configuration</title>
-
-  <para>
-   One of the most common exporters is the
-   <link xlink:href="https://github.com/prometheus/node_exporter">node
-   exporter</link>, it provides hardware and OS metrics from the host it's
-   running on. The exporter could be configured as follows:
-<programlisting>
-  services.prometheus.exporters.node = {
-    enable = true;
-    port = 9100;
-    enabledCollectors = [
-      "logind"
-      "systemd"
-    ];
-    disabledCollectors = [
-      "textfile"
-    ];
-    openFirewall = true;
-    firewallFilter = "-i br0 -p tcp -m tcp --dport 9100";
-  };
-</programlisting>
-   It should now serve all metrics from the collectors that are explicitly
-   enabled and the ones that are
-   <link xlink:href="https://github.com/prometheus/node_exporter#enabled-by-default">enabled
-   by default</link>, via http under <literal>/metrics</literal>. In this
-   example the firewall should just allow incoming connections to the
-   exporter's port on the bridge interface <literal>br0</literal> (this would
-   have to be configured separately of course). For more information about
-   configuration see <literal>man configuration.nix</literal> or search through
-   the
-   <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>
-
-  <para>
-   To add a new exporter, it has to be packaged first (see
-   <literal>nixpkgs/pkgs/servers/monitoring/prometheus/</literal> for
-   examples), then a module can be added. The postfix exporter is used in this
-   example:
-  </para>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Some default options for all exporters are provided by
-     <literal>nixpkgs/nixos/modules/services/monitoring/prometheus/exporters.nix</literal>:
-    </para>
-   </listitem>
-   <listitem override='none'>
-    <itemizedlist>
-     <listitem>
-      <para>
-       <literal>enable</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>port</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>listenAddress</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>extraFlags</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>openFirewall</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>firewallFilter</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>user</literal>
-      </para>
-     </listitem>
-     <listitem>
-      <para>
-       <literal>group</literal>
-      </para>
-     </listitem>
-    </itemizedlist>
-   </listitem>
-   <listitem>
-    <para>
-     As there is already a package available, the module can now be added. This
-     is accomplished by adding a new file to the
-     <literal>nixos/modules/services/monitoring/prometheus/exporters/</literal>
-     directory, which will be called postfix.nix and contains all exporter
-     specific options and configuration:
-<programlisting>
-# nixpgs/nixos/modules/services/prometheus/exporters/postfix.nix
-{ config, lib, pkgs, options }:
-
-with lib;
-
-let
-  # for convenience we define cfg here
-  cfg = config.services.prometheus.exporters.postfix;
-in
-{
-  port = 9154; # The postfix exporter listens on this port by default
-
-  # `extraOpts` is an attribute set which contains additional options
-  # (and optional overrides for default options).
-  # Note that this attribute is optional.
-  extraOpts = {
-    telemetryPath = mkOption {
-      type = types.str;
-      default = "/metrics";
-      description = ''
-        Path under which to expose metrics.
-      '';
-    };
-    logfilePath = mkOption {
-      type = types.path;
-      default = /var/log/postfix_exporter_input.log;
-      example = /var/log/mail.log;
-      description = ''
-        Path where Postfix writes log entries.
-        This file will be truncated by this exporter!
-      '';
-    };
-    showqPath = mkOption {
-      type = types.path;
-      default = /var/spool/postfix/public/showq;
-      example = /var/lib/postfix/queue/public/showq;
-      description = ''
-        Path at which Postfix places its showq socket.
-      '';
-    };
-  };
-
-  # `serviceOpts` is an attribute set which contains configuration
-  # for the exporter's systemd service. One of
-  # `serviceOpts.script` and `serviceOpts.serviceConfig.ExecStart`
-  # has to be specified here. This will be merged with the default
-  # service configuration.
-  # Note that by default 'DynamicUser' is 'true'.
-  serviceOpts = {
-    serviceConfig = {
-      DynamicUser = false;
-      ExecStart = ''
-        ${pkgs.prometheus-postfix-exporter}/bin/postfix_exporter \
-          --web.listen-address ${cfg.listenAddress}:${toString cfg.port} \
-          --web.telemetry-path ${cfg.telemetryPath} \
-          ${concatStringsSep " \\\n  " cfg.extraFlags}
-      '';
-    };
-  };
-}
-</programlisting>
-    </para>
-   </listitem>
-   <listitem>
-    <para>
-     This should already be enough for the postfix exporter. Additionally one
-     could now add assertions and conditional default values. This can be done
-     in the 'meta-module' that combines all exporter definitions and generates
-     the submodules:
-     <literal>nixpkgs/nixos/modules/services/prometheus/exporters.nix</literal>
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
- <section xml:id="module-services-prometheus-exporters-update-exporter-module">
-  <title>Updating an exporter module</title>
-   <para>
-     Should an exporter option change at some point, it is possible to add
-     information about the change to the exporter definition similar to
-     <literal>nixpkgs/nixos/modules/rename.nix</literal>:
-<programlisting>
-{ config, lib, pkgs, options }:
-
-with lib;
-
-let
-  cfg = config.services.prometheus.exporters.nginx;
-in
-{
-  port = 9113;
-  extraOpts = {
-    # additional module options
-    # ...
-  };
-  serviceOpts = {
-    # service configuration
-    # ...
-  };
-  imports = [
-    # 'services.prometheus.exporters.nginx.telemetryEndpoint' -> 'services.prometheus.exporters.nginx.telemetryPath'
-    (mkRenamedOptionModule [ "telemetryEndpoint" ] [ "telemetryPath" ])
-
-    # removed option 'services.prometheus.exporters.nginx.insecure'
-    (mkRemovedOptionModule [ "insecure" ] ''
-      This option was replaced by 'prometheus.exporters.nginx.sslVerify' which defaults to true.
-    '')
-    ({ options.warnings = options.warnings; })
-  ];
-}
-</programlisting>
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
index 0c2de683ecf..f67596f05a3 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix
@@ -58,10 +58,10 @@ in
     };
   };
   serviceOpts = let
-    collectSettingsArgs = if (cfg.collectdBinary.enable) then ''
+    collectSettingsArgs = optionalString (cfg.collectdBinary.enable) ''
       --collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \
       --collectd.security-level ${cfg.collectdBinary.securityLevel} \
-    '' else "";
+    '';
   in {
     serviceConfig = {
       ExecStart = ''
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix b/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix
index 537d72e85c8..6f403b3e58c 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/pihole.nix
@@ -6,6 +6,11 @@ let
   cfg = config.services.prometheus.exporters.pihole;
 in
 {
+  imports = [
+    (mkRemovedOptionModule [ "interval"] "This option has been removed.")
+    ({ options.warnings = options.warnings; options.assertions = options.assertions; })
+  ];
+
   port = 9617;
   extraOpts = {
     apiToken = mkOption {
@@ -13,15 +18,7 @@ in
       default = "";
       example = "580a770cb40511eb85290242ac130003580a770cb40511eb85290242ac130003";
       description = lib.mdDoc ''
-        pi-hole API token which can be used instead of a password
-      '';
-    };
-    interval = mkOption {
-      type = types.str;
-      default = "10s";
-      example = "30s";
-      description = lib.mdDoc ''
-        How often to scrape new data
+        Pi-Hole API token which can be used instead of a password
       '';
     };
     password = mkOption {
@@ -29,7 +26,7 @@ in
       default = "";
       example = "password";
       description = lib.mdDoc ''
-        The password to login into pihole. An api token can be used instead.
+        The password to login into Pi-Hole. An api token can be used instead.
       '';
     };
     piholeHostname = mkOption {
@@ -37,7 +34,7 @@ in
       default = "pihole";
       example = "127.0.0.1";
       description = lib.mdDoc ''
-        Hostname or address where to find the pihole webinterface
+        Hostname or address where to find the Pi-Hole webinterface
       '';
     };
     piholePort = mkOption {
@@ -45,7 +42,7 @@ in
       default = 80;
       example = 443;
       description = lib.mdDoc ''
-        The port pihole webinterface is reachable on
+        The port Pi-Hole webinterface is reachable on
       '';
     };
     protocol = mkOption {
@@ -53,21 +50,28 @@ in
       default = "http";
       example = "https";
       description = lib.mdDoc ''
-        The protocol which is used to connect to pihole
+        The protocol which is used to connect to Pi-Hole
+      '';
+    };
+    timeout = mkOption {
+      type = types.str;
+      default = "5s";
+      description = lib.mdDoc ''
+        Controls the timeout to connect to a Pi-Hole instance
       '';
     };
   };
   serviceOpts = {
     serviceConfig = {
       ExecStart = ''
-        ${pkgs.bash}/bin/bash -c "${pkgs.prometheus-pihole-exporter}/bin/pihole-exporter \
-          -interval ${cfg.interval} \
+        ${pkgs.prometheus-pihole-exporter}/bin/pihole-exporter \
           ${optionalString (cfg.apiToken != "") "-pihole_api_token ${cfg.apiToken}"} \
           -pihole_hostname ${cfg.piholeHostname} \
           ${optionalString (cfg.password != "") "-pihole_password ${cfg.password}"} \
           -pihole_port ${toString cfg.piholePort} \
           -pihole_protocol ${cfg.protocol} \
-          -port ${toString cfg.port}"
+          -port ${toString cfg.port} \
+          -timeout ${cfg.timeout}
       '';
     };
   };
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
index 0b48827f43f..f9dcfad07d3 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/rspamd.nix
@@ -9,7 +9,7 @@ let
     pkgs.writeText "rspamd-exporter-config.yml" (builtins.toJSON conf);
 
   generateConfig = extraLabels: {
-    metrics = (map (path: {
+    modules.default.metrics = (map (path: {
       name = "rspamd_${replaceStrings [ "[" "." " " "]" "\\" "'" ] [ "_" "_" "_" "" "" "" ] path}";
       path = "{ .${path} }";
       labels = extraLabels;
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/shelly.nix b/nixos/modules/services/monitoring/prometheus/exporters/shelly.nix
new file mode 100644
index 00000000000..b9cfd1b1e84
--- /dev/null
+++ b/nixos/modules/services/monitoring/prometheus/exporters/shelly.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, options }:
+
+with lib;
+
+let
+  cfg = config.services.prometheus.exporters.shelly;
+in
+{
+  port = 9784;
+  extraOpts = {
+    metrics-file = mkOption {
+      type = types.path;
+      description = lib.mdDoc ''
+        Path to the JSON file with the metric definitions
+      '';
+    };
+  };
+  serviceOpts = {
+    serviceConfig = {
+      ExecStart = ''
+        ${pkgs.prometheus-shelly-exporter}/bin/shelly_exporter \
+          -metrics-file ${cfg.metrics-file} \
+          -listen-address ${cfg.listenAddress}:${toString cfg.port}
+      '';
+    };
+  };
+}
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
index 0c5648c1414..50e1321a1e9 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix
@@ -4,12 +4,12 @@ with lib;
 
 let
   cfg = config.services.prometheus.exporters.smartctl;
-  args = concatStrings [
-    "--web.listen-address=\"${cfg.listenAddress}:${toString cfg.port}\" "
-    "--smartctl.path=\"${pkgs.smartmontools}/bin/smartctl\" "
-    "--smartctl.interval=\"${cfg.maxInterval}\" "
-    "${concatMapStringsSep " " (device: "--smartctl.device=${device}") cfg.devices}"
-  ];
+  args = lib.escapeShellArgs ([
+    "--web.listen-address=${cfg.listenAddress}:${toString cfg.port}"
+    "--smartctl.path=${pkgs.smartmontools}/bin/smartctl"
+    "--smartctl.interval=${cfg.maxInterval}"
+  ] ++ map (device: "--smartctl.device=${device}") cfg.devices
+  ++ cfg.extraFlags);
 in {
   port = 9633;
 
diff --git a/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix b/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix
index 5cd1e2c65e9..3b7f978528c 100644
--- a/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix
+++ b/nixos/modules/services/monitoring/prometheus/exporters/unpoller.nix
@@ -24,9 +24,9 @@ in {
     inherit (options.services.unpoller.unifi) controllers;
     inherit (options.services.unpoller) loki;
     log = {
-      debug = mkEnableOption (lib.mdDoc "debug logging including line numbers, high resolution timestamps, per-device logs.");
-      quiet = mkEnableOption (lib.mdDoc "startup and error logs only.");
-      prometheusErrors = mkEnableOption (lib.mdDoc "emitting errors to prometheus.");
+      debug = mkEnableOption (lib.mdDoc "debug logging including line numbers, high resolution timestamps, per-device logs");
+      quiet = mkEnableOption (lib.mdDoc "startup and error logs only");
+      prometheusErrors = mkEnableOption (lib.mdDoc "emitting errors to prometheus");
     };
   };
 
diff --git a/nixos/modules/services/monitoring/tuptime.nix b/nixos/modules/services/monitoring/tuptime.nix
index d97e408bce3..97cc3752625 100644
--- a/nixos/modules/services/monitoring/tuptime.nix
+++ b/nixos/modules/services/monitoring/tuptime.nix
@@ -54,8 +54,8 @@ in {
             Type = "oneshot";
             User = "_tuptime";
             RemainAfterExit = true;
-            ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
-            ExecStop = "${pkgs.tuptime}/bin/tuptime -xg";
+            ExecStart = "${pkgs.tuptime}/bin/tuptime -q";
+            ExecStop = "${pkgs.tuptime}/bin/tuptime -qg";
           };
         };
 
@@ -64,7 +64,7 @@ in {
           serviceConfig = {
             Type = "oneshot";
             User = "_tuptime";
-            ExecStart = "${pkgs.tuptime}/bin/tuptime -x";
+            ExecStart = "${pkgs.tuptime}/bin/tuptime -q";
           };
         };
       };
diff --git a/nixos/modules/services/monitoring/uptime-kuma.nix b/nixos/modules/services/monitoring/uptime-kuma.nix
index b6dc993e6a0..5f803d57b5e 100644
--- a/nixos/modules/services/monitoring/uptime-kuma.nix
+++ b/nixos/modules/services/monitoring/uptime-kuma.nix
@@ -7,9 +7,11 @@ let
 in
 {
 
+  meta.maintainers = [ lib.maintainers.julienmalka ];
+
   options = {
     services.uptime-kuma = {
-      enable = mkEnableOption (mdDoc "Uptime Kuma, this assumes a reverse proxy to be set.");
+      enable = mkEnableOption (mdDoc "Uptime Kuma, this assumes a reverse proxy to be set");
 
       package = mkOption {
         type = types.package;
@@ -18,9 +20,10 @@ in
         description = lib.mdDoc "Uptime Kuma package to use.";
       };
 
+      appriseSupport = mkEnableOption (mdDoc "apprise support for notifications");
+
       settings = lib.mkOption {
-        type =
-          lib.types.submodule { freeformType = with lib.types; attrsOf str; };
+        type = lib.types.submodule { freeformType = with lib.types; attrsOf str; };
         default = { };
         example = {
           PORT = "4000";
@@ -28,7 +31,7 @@ in
         };
         description = lib.mdDoc ''
           Additional configuration for Uptime Kuma, see
-          <https://github.com/louislam/uptime-kuma/wiki/Environment-Variables">
+          <https://github.com/louislam/uptime-kuma/wiki/Environment-Variables>
           for supported values.
         '';
       };
@@ -47,6 +50,7 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       environment = cfg.settings;
+      path = with pkgs; [ unixtools.ping ] ++ lib.optional cfg.appriseSupport apprise;
       serviceConfig = {
         Type = "simple";
         StateDirectory = "uptime-kuma";
diff --git a/nixos/modules/services/network-filesystems/kubo.nix b/nixos/modules/services/network-filesystems/kubo.nix
index 13a062c3212..2537bb1b8d8 100644
--- a/nixos/modules/services/network-filesystems/kubo.nix
+++ b/nixos/modules/services/network-filesystems/kubo.nix
@@ -5,6 +5,35 @@ let
 
   settingsFormat = pkgs.formats.json {};
 
+  rawDefaultConfig = lib.importJSON (pkgs.runCommand "kubo-default-config" {
+    nativeBuildInputs = [ cfg.package ];
+  } ''
+    export IPFS_PATH="$TMPDIR"
+    ipfs init --empty-repo --profile=${profile}
+    ipfs --offline config show > "$out"
+  '');
+
+  # Remove the PeerID (an attribute of "Identity") of the temporary Kubo repo.
+  # The "Pinning" section contains the "RemoteServices" section, which would prevent
+  # the daemon from starting as that setting can't be changed via ipfs config replace.
+  defaultConfig = builtins.removeAttrs rawDefaultConfig [ "Identity" "Pinning" ];
+
+  customizedConfig = lib.recursiveUpdate defaultConfig cfg.settings;
+
+  configFile = settingsFormat.generate "kubo-config.json" customizedConfig;
+
+  # Create a fake repo containing only the file "api".
+  # $IPFS_PATH will point to this directory instead of the real one.
+  # For some reason the Kubo CLI tools insist on reading the
+  # config file when it exists. But the Kubo daemon sets the file
+  # permissions such that only the ipfs user is allowed to read
+  # this file. This prevents normal users from talking to the daemon.
+  # To work around this terrible design, create a fake repo with no
+  # config file, only an api file and everything should work as expected.
+  fakeKuboRepo = pkgs.writeTextDir "api" ''
+    /unix/run/ipfs.sock
+  '';
+
   kuboFlags = utils.escapeSystemdExecArgs (
     optional cfg.autoMount "--mount" ++
     optional cfg.enableGC "--enable-gc" ++
@@ -21,6 +50,22 @@ let
 
   splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw);
 
+  multiaddrsToListenStreams = addrIn:
+    let
+      addrs = if builtins.typeOf addrIn == "list"
+      then addrIn else [ addrIn ];
+      unfilteredResult = map multiaddrToListenStream addrs;
+    in
+      builtins.filter (addr: addr != null) unfilteredResult;
+
+  multiaddrsToListenDatagrams = addrIn:
+    let
+      addrs = if builtins.typeOf addrIn == "list"
+      then addrIn else [ addrIn ];
+      unfilteredResult = map multiaddrToListenDatagram addrs;
+    in
+      builtins.filter (addr: addr != null) unfilteredResult;
+
   multiaddrToListenStream = addrRaw:
     let
       addr = splitMulitaddr addrRaw;
@@ -137,13 +182,18 @@ in
 
           options = {
             Addresses.API = mkOption {
-              type = types.str;
-              default = "/ip4/127.0.0.1/tcp/5001";
-              description = lib.mdDoc "Where Kubo exposes its API to";
+              type = types.oneOf [ types.str (types.listOf types.str) ];
+              default = [ ];
+              description = lib.mdDoc ''
+                Multiaddr or array of multiaddrs describing the address to serve the local HTTP API on.
+                In addition to the multiaddrs listed here, the daemon will also listen on a Unix domain socket.
+                To allow the ipfs CLI tools to communicate with the daemon over that socket,
+                add your user to the correct group, e.g. `users.users.alice.extraGroups = [ config.services.kubo.group ];`
+              '';
             };
 
             Addresses.Gateway = mkOption {
-              type = types.str;
+              type = types.oneOf [ types.str (types.listOf types.str) ];
               default = "/ip4/127.0.0.1/tcp/8080";
               description = lib.mdDoc "Where the IPFS Gateway can be reached";
             };
@@ -154,16 +204,20 @@ in
                 "/ip4/0.0.0.0/tcp/4001"
                 "/ip6/::/tcp/4001"
                 "/ip4/0.0.0.0/udp/4001/quic"
+                "/ip4/0.0.0.0/udp/4001/quic-v1"
+                "/ip4/0.0.0.0/udp/4001/quic-v1/webtransport"
                 "/ip6/::/udp/4001/quic"
+                "/ip6/::/udp/4001/quic-v1"
+                "/ip6/::/udp/4001/quic-v1/webtransport"
               ];
               description = lib.mdDoc "Where Kubo listens for incoming p2p connections";
             };
           };
         };
         description = lib.mdDoc ''
-          Attrset of daemon configuration to set using {command}`ipfs config`, every time the daemon starts.
+          Attrset of daemon configuration.
           See [https://github.com/ipfs/kubo/blob/master/docs/config.md](https://github.com/ipfs/kubo/blob/master/docs/config.md) for reference.
-          Keep in mind that this configuration is stateful; i.e., unsetting anything in here does not reset the value to the default!
+          You can't set `Identity` or `Pinning`.
         '';
         default = { };
         example = {
@@ -211,8 +265,23 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = !builtins.hasAttr "Identity" cfg.settings;
+        message = ''
+          You can't set services.kubo.settings.Identity because the ``config replace`` subcommand used at startup does not support modifying any of the Identity settings.
+        '';
+      }
+      {
+        assertion = !((builtins.hasAttr "Pinning" cfg.settings) && (builtins.hasAttr "RemoteServices" cfg.settings.Pinning));
+        message = ''
+          You can't set services.kubo.settings.Pinning.RemoteServices because the ``config replace`` subcommand used at startup does not work with it.
+        '';
+      }
+    ];
+
     environment.systemPackages = [ cfg.package ];
-    environment.variables.IPFS_PATH = cfg.dataDir;
+    environment.variables.IPFS_PATH = fakeKuboRepo;
 
     # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size
     boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000;
@@ -262,21 +331,30 @@ in
 
       preStart = ''
         if [[ ! -f "$IPFS_PATH/config" ]]; then
-          ipfs init ${optionalString cfg.emptyRepo "-e"} --profile=${profile}
+          ipfs init ${optionalString cfg.emptyRepo "-e"}
         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"
       '' + optionalString cfg.autoMigrate ''
         ${pkgs.kubo-migrator}/bin/fs-repo-migrations -to '${cfg.package.repoVersion}' -y
       '' + ''
-          ipfs --offline config profile apply ${profile} >/dev/null
         fi
-      '' + ''
-        ipfs --offline config show \
-          | ${pkgs.jq}/bin/jq '. * $settings' --argjson settings ${
-              escapeShellArg (builtins.toJSON cfg.settings)
-            } \
-          | ipfs --offline config replace -
+        ipfs --offline config show |
+          ${pkgs.jq}/bin/jq -s '.[0].Pinning as $Pinning | .[0].Identity as $Identity | .[1] + {$Identity,$Pinning}' - '${configFile}' |
+
+          # This command automatically injects the private key and other secrets from
+          # the old config file back into the new config file.
+          # Unfortunately, it doesn't keep the original `Identity.PeerID`,
+          # so we need `ipfs config show` and jq above.
+          # See https://github.com/ipfs/kubo/issues/8993 for progress on fixing this problem.
+          # Kubo also wants a specific version of the original "Pinning.RemoteServices"
+          # section (redacted by `ipfs config show`), such that that section doesn't
+          # change when the changes are applied. Whyyyyyy.....
+          ipfs --offline config replace -
+      '';
+      postStop = mkIf cfg.autoMount ''
+        # After an unclean shutdown the fuse mounts at cfg.ipnsMountDir and cfg.ipfsMountDir are locked
+        umount --quiet '${cfg.ipnsMountDir}' '${cfg.ipfsMountDir}' || true
       '';
       serviceConfig = {
         ExecStart = [ "" "${cfg.package}/bin/ipfs daemon ${kuboFlags}" ];
@@ -293,27 +371,23 @@ in
       wantedBy = [ "sockets.target" ];
       socketConfig = {
         ListenStream =
-          let
-            fromCfg = multiaddrToListenStream cfg.settings.Addresses.Gateway;
-          in
-          [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
+          [ "" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.Gateway);
         ListenDatagram =
-          let
-            fromCfg = multiaddrToListenDatagram cfg.settings.Addresses.Gateway;
-          in
-          [ "" ] ++ lib.optional (fromCfg != null) fromCfg;
+          [ "" ] ++ (multiaddrsToListenDatagrams cfg.settings.Addresses.Gateway);
       };
     };
 
     systemd.sockets.ipfs-api = {
       wantedBy = [ "sockets.target" ];
-      # We also include "%t/ipfs.sock" because there is no way to put the "%t"
-      # in the multiaddr.
-      socketConfig.ListenStream =
-        let
-          fromCfg = multiaddrToListenStream cfg.settings.Addresses.API;
-        in
-        [ "" "%t/ipfs.sock" ] ++ lib.optional (fromCfg != null) fromCfg;
+      socketConfig = {
+        # We also include "%t/ipfs.sock" because there is no way to put the "%t"
+        # in the multiaddr.
+        ListenStream =
+          [ "" "%t/ipfs.sock" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.API);
+        SocketMode = "0660";
+        SocketUser = cfg.user;
+        SocketGroup = cfg.group;
+      };
     };
   };
 
diff --git a/nixos/modules/services/network-filesystems/litestream/litestream.xml b/nixos/modules/services/network-filesystems/litestream/default.md
index 8f5597bb689..8d8486507b7 100644
--- a/nixos/modules/services/network-filesystems/litestream/litestream.xml
+++ b/nixos/modules/services/network-filesystems/litestream/default.md
@@ -1,23 +1,14 @@
-<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-litestream">
- <title>Litestream</title>
- <para>
-  <link xlink:href="https://litestream.io/">Litestream</link> is a standalone streaming
-  replication tool for SQLite.
- </para>
+# Litestream {#module-services-litestream}
 
- <section xml:id="module-services-litestream-configuration">
-  <title>Configuration</title>
+[Litestream](https://litestream.io/) is a standalone streaming
+replication tool for SQLite.
 
-  <para>
-   Litestream service is managed by a dedicated user named <literal>litestream</literal>
-   which needs permission to the database file. Here's an example config which gives
-   required permissions to access <link linkend="opt-services.grafana.settings.database.path">
-   grafana database</link>:
-<programlisting>
+## Configuration {#module-services-litestream-configuration}
+
+Litestream service is managed by a dedicated user named `litestream`
+which needs permission to the database file. Here's an example config which gives
+required permissions to access [grafana database](#opt-services.grafana.settings.database.path):
+```
 { pkgs, ... }:
 {
   users.users.litestream.extraGroups = [ "grafana" ];
@@ -58,8 +49,4 @@
     };
   };
 }
-</programlisting>
-  </para>
- </section>
-
-</chapter>
+```
diff --git a/nixos/modules/services/network-filesystems/litestream/default.nix b/nixos/modules/services/network-filesystems/litestream/default.nix
index 884ffa50e7c..6e2ec1ccaa3 100644
--- a/nixos/modules/services/network-filesystems/litestream/default.nix
+++ b/nixos/modules/services/network-filesystems/litestream/default.nix
@@ -94,5 +94,6 @@ in
     };
     users.groups.litestream = {};
   };
-  meta.doc = ./litestream.xml;
+
+  meta.doc = ./default.md;
 }
diff --git a/nixos/modules/services/network-filesystems/moosefs.nix b/nixos/modules/services/network-filesystems/moosefs.nix
index ab82a2a07dd..49cbc89d5a9 100644
--- a/nixos/modules/services/network-filesystems/moosefs.nix
+++ b/nixos/modules/services/network-filesystems/moosefs.nix
@@ -85,7 +85,7 @@ in {
         description = lib.mdDoc "Run daemons as user moosefs instead of root.";
       };
 
-      client.enable = mkEnableOption (lib.mdDoc "Moosefs client.");
+      client.enable = mkEnableOption (lib.mdDoc "Moosefs client");
 
       master = {
         enable = mkOption {
@@ -131,7 +131,7 @@ in {
       };
 
       metalogger = {
-        enable = mkEnableOption (lib.mdDoc "Moosefs metalogger daemon.");
+        enable = mkEnableOption (lib.mdDoc "Moosefs metalogger daemon");
 
         settings = mkOption {
           type = types.submodule {
@@ -149,7 +149,7 @@ in {
       };
 
       chunkserver = {
-        enable = mkEnableOption (lib.mdDoc "Moosefs chunkserver daemon.");
+        enable = mkEnableOption (lib.mdDoc "Moosefs chunkserver daemon");
 
         openFirewall = mkOption {
           type = types.bool;
diff --git a/nixos/modules/services/network-filesystems/openafs/lib.nix b/nixos/modules/services/network-filesystems/openafs/lib.nix
index 80628f4dfaf..e5e147a8dc3 100644
--- a/nixos/modules/services/network-filesystems/openafs/lib.nix
+++ b/nixos/modules/services/network-filesystems/openafs/lib.nix
@@ -1,13 +1,13 @@
 { config, lib, ...}:
 
 let
-  inherit (lib) concatStringsSep mkOption types;
+  inherit (lib) concatStringsSep mkOption types optionalString;
 
 in {
 
   mkCellServDB = cellName: db: ''
     >${cellName}
-  '' + (concatStringsSep "\n" (map (dbm: if (dbm.ip != "" && dbm.dnsname != "") then dbm.ip + " #" + dbm.dnsname else "")
+  '' + (concatStringsSep "\n" (map (dbm: optionalString (dbm.ip != "" && dbm.dnsname != "") "${dbm.ip} #${dbm.dnsname}")
                                    db))
      + "\n";
 
diff --git a/nixos/modules/services/network-filesystems/openafs/server.nix b/nixos/modules/services/network-filesystems/openafs/server.nix
index 1c615d3bfb6..ad0fd783567 100644
--- a/nixos/modules/services/network-filesystems/openafs/server.nix
+++ b/nixos/modules/services/network-filesystems/openafs/server.nix
@@ -4,7 +4,8 @@
 with import ./lib.nix { inherit config lib pkgs; };
 
 let
-  inherit (lib) concatStringsSep literalExpression mkIf mkOption optionalString types;
+  inherit (lib) concatStringsSep literalExpression mkIf mkOption mkEnableOption
+  optionalString types;
 
   bosConfig = pkgs.writeText "BosConfig" (''
     restrictmode 1
@@ -24,9 +25,15 @@ let
     parm ${openafsSrv}/libexec/openafs/salvageserver ${cfg.roles.fileserver.salvageserverArgs}
     parm ${openafsSrv}/libexec/openafs/dasalvager ${cfg.roles.fileserver.salvagerArgs}
     end
-  '') + (optionalString (cfg.roles.database.enable && cfg.roles.backup.enable) ''
+  '') + (optionalString (cfg.roles.database.enable && cfg.roles.backup.enable && (!cfg.roles.backup.enableFabs)) ''
     bnode simple buserver 1
-    parm ${openafsSrv}/libexec/openafs/buserver ${cfg.roles.backup.buserverArgs} ${optionalString (cfg.roles.backup.cellServDB != []) "-cellservdb /etc/openafs/backup/"}
+    parm ${openafsSrv}/libexec/openafs/buserver ${cfg.roles.backup.buserverArgs} ${optionalString useBuCellServDB "-cellservdb /etc/openafs/backup/"}
+    end
+  '') + (optionalString (cfg.roles.database.enable &&
+                         cfg.roles.backup.enable &&
+                         cfg.roles.backup.enableFabs) ''
+    bnode simple buserver 1
+    parm ${lib.getBin pkgs.fabs}/bin/fabsys server --config ${fabsConfFile} ${cfg.roles.backup.fabsArgs}
     end
   ''));
 
@@ -34,12 +41,27 @@ let
     pkgs.writeText "NetInfo" ((concatStringsSep "\nf " cfg.advertisedAddresses) + "\n")
   else null;
 
-  buCellServDB = pkgs.writeText "backup-cellServDB-${cfg.cellName}" (mkCellServDB cfg.cellName cfg.roles.backup.cellServDB);
+  buCellServDB = pkgs.writeText "backup-cellServDB-${cfg.cellName}"
+    (mkCellServDB cfg.cellName cfg.roles.backup.cellServDB);
+
+  useBuCellServDB = (cfg.roles.backup.cellServDB != []) && (!cfg.roles.backup.enableFabs);
 
   cfg = config.services.openafsServer;
 
   udpSizeStr = toString cfg.udpPacketSize;
 
+  fabsConfFile = pkgs.writeText "fabs.yaml" (builtins.toJSON ({
+    afs = {
+      aklog = cfg.package + "/bin/aklog";
+      cell = cfg.cellName;
+      dumpscan = cfg.package + "/bin/afsdump_scan";
+      fs = cfg.package + "/bin/fs";
+      pts = cfg.package + "/bin/pts";
+      vos = cfg.package + "/bin/vos";
+    };
+    k5start.command = (lib.getBin pkgs.kstart) + "/bin/k5start";
+  } // cfg.roles.backup.fabsExtraConfig));
+
 in {
 
   options = {
@@ -80,8 +102,8 @@ in {
       };
 
       package = mkOption {
-        default = pkgs.openafs.server or pkgs.openafs;
-        defaultText = literalExpression "pkgs.openafs.server or pkgs.openafs";
+        default = pkgs.openafs;
+        defaultText = literalExpression "pkgs.openafs";
         type = types.package;
         description = lib.mdDoc "OpenAFS package for the server binaries";
       };
@@ -154,16 +176,20 @@ in {
         };
 
         backup = {
-          enable = mkOption {
-            default = false;
-            type = types.bool;
-            description = lib.mdDoc ''
-              Backup server role. Use in conjunction with the
-              `database` role to maintain the Backup
-              Database. Normally only used in conjunction with tape storage
-              or IBM's Tivoli Storage Manager.
-            '';
-          };
+          enable = mkEnableOption (lib.mdDoc ''
+            Backup server role. When using OpenAFS built-in buserver, use in conjunction with the
+            `database` role to maintain the Backup
+            Database. Normally only used in conjunction with tape storage
+            or IBM's Tivoli Storage Manager.
+
+            For a modern backup server, enable this role and see
+            {option}`enableFabs`.
+          '');
+
+          enableFabs = mkEnableOption (lib.mdDoc ''
+            FABS, the flexible AFS backup system. It stores volumes as dump files, relying on other
+            pre-existing backup solutions for handling them.
+          '');
 
           buserverArgs = mkOption {
             default = "";
@@ -181,6 +207,30 @@ in {
               other database server machines.
             '';
           };
+
+          fabsArgs = mkOption {
+            default = "";
+            type = types.str;
+            description = lib.mdDoc ''
+              Arguments to the fabsys process. See
+              {manpage}`fabsys_server(1)` and
+              {manpage}`fabsys_config(1)`.
+            '';
+          };
+
+          fabsExtraConfig = mkOption {
+            default = {};
+            type = types.attrs;
+            description = lib.mdDoc ''
+              Additional configuration parameters for the FABS backup server.
+            '';
+            example = literalExpression ''
+            {
+              afs.localauth = true;
+              afs.keytab = config.sops.secrets.fabsKeytab.path;
+            }
+            '';
+          };
         };
       };
 
@@ -239,7 +289,7 @@ in {
         mode = "0644";
       };
       buCellServDB = {
-        enable = (cfg.roles.backup.cellServDB != []);
+        enable = useBuCellServDB;
         text = mkCellServDB cfg.cellName cfg.roles.backup.cellServDB;
         target = "openafs/backup/CellServDB";
       };
@@ -257,7 +307,7 @@ in {
         preStart = ''
           mkdir -m 0755 -p /var/openafs
           ${optionalString (netInfo != null) "cp ${netInfo} /var/openafs/netInfo"}
-          ${optionalString (cfg.roles.backup.cellServDB != []) "cp ${buCellServDB}"}
+          ${optionalString useBuCellServDB "cp ${buCellServDB}"}
         '';
         serviceConfig = {
           ExecStart = "${openafsBin}/bin/bosserver -nofork";
diff --git a/nixos/modules/services/networking/avahi-daemon.nix b/nixos/modules/services/networking/avahi-daemon.nix
index 3933ed5a231..3a7519c7230 100644
--- a/nixos/modules/services/networking/avahi-daemon.nix
+++ b/nixos/modules/services/networking/avahi-daemon.nix
@@ -5,7 +5,7 @@ with lib;
 let
   cfg = config.services.avahi;
 
-  yesNo = yes : if yes then "yes" else "no";
+  yesNo = yes: if yes then "yes" else "no";
 
   avahiDaemonConf = with cfg; pkgs.writeText "avahi-daemon.conf" ''
     [server]
@@ -17,7 +17,8 @@ let
     browse-domains=${concatStringsSep ", " browseDomains}
     use-ipv4=${yesNo ipv4}
     use-ipv6=${yesNo ipv6}
-    ${optionalString (interfaces!=null) "allow-interfaces=${concatStringsSep "," interfaces}"}
+    ${optionalString (allowInterfaces!=null) "allow-interfaces=${concatStringsSep "," allowInterfaces}"}
+    ${optionalString (denyInterfaces!=null) "deny-interfaces=${concatStringsSep "," denyInterfaces}"}
     ${optionalString (domainName!=null) "domain-name=${domainName}"}
     allow-point-to-point=${yesNo allowPointToPoint}
     ${optionalString (cacheEntriesMax!=null) "cache-entries-max=${toString cacheEntriesMax}"}
@@ -39,6 +40,10 @@ let
   '';
 in
 {
+  imports = [
+    (lib.mkRenamedOptionModule [ "services" "avahi" "interfaces" ] [ "services" "avahi" "allowInterfaces" ])
+  ];
+
   options.services.avahi = {
     enable = mkOption {
       type = types.bool;
@@ -47,7 +52,7 @@ in
         Whether to run the Avahi daemon, which allows Avahi clients
         to use Avahi's service discovery facilities and also allows
         the local machine to advertise its presence and services
-        (through the mDNS responder implemented by `avahi-daemon').
+        (through the mDNS responder implemented by `avahi-daemon`).
       '';
     };
 
@@ -91,7 +96,7 @@ in
       description = lib.mdDoc "Whether to use IPv6.";
     };
 
-    interfaces = mkOption {
+    allowInterfaces = mkOption {
       type = types.nullOr (types.listOf types.str);
       default = null;
       description = lib.mdDoc ''
@@ -101,6 +106,17 @@ in
       '';
     };
 
+    denyInterfaces = mkOption {
+      type = types.nullOr (types.listOf types.str);
+      default = null;
+      description = lib.mdDoc ''
+        List of network interfaces that should be ignored by the
+        {command}`avahi-daemon`. Other unspecified interfaces will be used,
+        unless {option}`allowInterfaces` is set. This option takes precedence
+        over {option}`allowInterfaces`.
+      '';
+    };
+
     openFirewall = mkOption {
       type = types.bool;
       default = true;
@@ -134,7 +150,7 @@ in
 
     extraServiceFiles = mkOption {
       type = with types; attrsOf (either str path);
-      default = {};
+      default = { };
       example = literalExpression ''
         {
           ssh = "''${pkgs.avahi}/etc/avahi/services/ssh.service";
@@ -205,7 +221,7 @@ in
       default = false;
       description = lib.mdDoc ''
         Whether to enable the mDNS NSS (Name Service Switch) plug-in.
-        Enabling it allows applications to resolve names in the `.local'
+        Enabling it allows applications to resolve names in the `.local`
         domain by transparently querying the Avahi daemon.
       '';
     };
@@ -236,7 +252,7 @@ in
       isSystemUser = true;
     };
 
-    users.groups.avahi = {};
+    users.groups.avahi = { };
 
     system.nssModules = optional cfg.nssmdns pkgs.nssmdns;
     system.nssDatabases.hosts = optionals cfg.nssmdns (mkMerge [
@@ -246,10 +262,12 @@ in
 
     environment.systemPackages = [ pkgs.avahi ];
 
-    environment.etc = (mapAttrs' (n: v: nameValuePair
-      "avahi/services/${n}.service"
-      { ${if types.path.check v then "source" else "text"} = v; }
-    ) cfg.extraServiceFiles);
+    environment.etc = (mapAttrs'
+      (n: v: nameValuePair
+        "avahi/services/${n}.service"
+        { ${if types.path.check v then "source" else "text"} = v; }
+      )
+      cfg.extraServiceFiles);
 
     systemd.sockets.avahi-daemon = {
       description = "Avahi mDNS/DNS-SD Stack Activation Socket";
@@ -275,6 +293,7 @@ in
         BusName = "org.freedesktop.Avahi";
         Type = "dbus";
         ExecStart = "${pkgs.avahi}/sbin/avahi-daemon --syslog -f ${avahiDaemonConf}";
+        ConfigurationDirectory = "avahi/services";
       };
     };
 
diff --git a/nixos/modules/services/networking/bind.nix b/nixos/modules/services/networking/bind.nix
index f963e341546..f1829747bb1 100644
--- a/nixos/modules/services/networking/bind.nix
+++ b/nixos/modules/services/networking/bind.nix
@@ -36,6 +36,17 @@ let
         description = lib.mdDoc "Addresses who may request zone transfers.";
         default = [ ];
       };
+      allowQuery = mkOption {
+        type = types.listOf types.str;
+        description = lib.mdDoc ''
+          List of address ranges allowed to query this zone. Instead of the address(es), this may instead
+          contain the single string "any".
+
+          NOTE: This overrides the global-level `allow-query` setting, which is set to the contents
+          of `cachenetworks`.
+        '';
+        default = [ "any" ];
+      };
       extraConfig = mkOption {
         type = types.str;
         description = lib.mdDoc "Extra zone config to be appended at the end of the zone section.";
@@ -69,7 +80,7 @@ let
       ${cfg.extraConfig}
 
       ${ concatMapStrings
-          ({ name, file, master ? true, slaves ? [], masters ? [], extraConfig ? "" }:
+          ({ name, file, master ? true, slaves ? [], masters ? [], allowQuery ? [], extraConfig ? "" }:
             ''
               zone "${name}" {
                 type ${if master then "master" else "slave"};
@@ -87,7 +98,7 @@ let
                      };
                    ''
                 }
-                allow-query { any; };
+                allow-query { ${concatMapStrings (ip: "${ip}; ") allowQuery}};
                 ${extraConfig}
               };
             '')
@@ -120,7 +131,9 @@ in
         description = lib.mdDoc ''
           What networks are allowed to use us as a resolver.  Note
           that this is for recursive queries -- all networks are
-          allowed to query zones configured with the `zones` option.
+          allowed to query zones configured with the `zones` option
+          by default (although this may be overridden within each
+          zone's configuration, via the `allowQuery` option).
           It is recommended that you limit cacheNetworks to avoid your
           server being used for DNS amplification attacks.
         '';
diff --git a/nixos/modules/services/networking/bird-lg.nix b/nixos/modules/services/networking/bird-lg.nix
index 11cfe3e7ec0..dc861dbfd11 100644
--- a/nixos/modules/services/networking/bird-lg.nix
+++ b/nixos/modules/services/networking/bird-lg.nix
@@ -4,6 +4,49 @@ with lib;
 
 let
   cfg = config.services.bird-lg;
+
+  stringOrConcat = sep: v: if builtins.isString v then v else concatStringsSep sep v;
+
+  frontend_args = let
+    fe = cfg.frontend;
+  in {
+    "--servers" = concatStringsSep "," fe.servers;
+    "--domain" = fe.domain;
+    "--listen" = fe.listenAddress;
+    "--proxy-port" = fe.proxyPort;
+    "--whois" = fe.whois;
+    "--dns-interface" = fe.dnsInterface;
+    "--bgpmap-info" = concatStringsSep "," cfg.frontend.bgpMapInfo;
+    "--title-brand" = fe.titleBrand;
+    "--navbar-brand" = fe.navbar.brand;
+    "--navbar-brand-url" = fe.navbar.brandURL;
+    "--navbar-all-servers" = fe.navbar.allServers;
+    "--navbar-all-url" = fe.navbar.allServersURL;
+    "--net-specific-mode" = fe.netSpecificMode;
+    "--protocol-filter" = concatStringsSep "," cfg.frontend.protocolFilter;
+  };
+
+  proxy_args = let
+    px = cfg.proxy;
+  in {
+    "--allowed" = concatStringsSep "," px.allowedIPs;
+    "--bird" = px.birdSocket;
+    "--listen" = px.listenAddress;
+    "--traceroute_bin" = px.traceroute.binary;
+    "--traceroute_flags" = concatStringsSep " " px.traceroute.flags;
+    "--traceroute_raw" = px.traceroute.rawOutput;
+  };
+
+  mkArgValue = value:
+    if isString value
+      then escapeShellArg value
+      else if isBool value
+        then boolToString value
+        else toString value;
+
+  filterNull = filterAttrs (_: v: v != "" && v != null && v != []);
+
+  argsAttrToList = args: mapAttrsToList (name: value: "${name} " + mkArgValue value ) (filterNull args);
 in
 {
   options = {
@@ -44,14 +87,12 @@ in
 
         domain = mkOption {
           type = types.str;
-          default = "";
           example = "dn42.lantian.pub";
           description = lib.mdDoc "Server name domain suffixes.";
         };
 
         servers = mkOption {
           type = types.listOf types.str;
-          default = [ ];
           example = [ "gigsgigscloud" "hostdare" ];
           description = lib.mdDoc "Server name prefixes.";
         };
@@ -134,10 +175,14 @@ in
         };
 
         extraArgs = mkOption {
-          type = types.lines;
-          default = "";
+          type = with types; either lines (listOf str);
+          default = [ ];
           description = lib.mdDoc ''
             Extra parameters documented [here](https://github.com/xddxdd/bird-lg-go#frontend).
+
+            :::{.note}
+            Passing lines (plain strings) is deprecated in favour of passing lists of strings.
+            :::
           '';
         };
       };
@@ -160,8 +205,7 @@ in
 
         birdSocket = mkOption {
           type = types.str;
-          default = "/run/bird.ctl";
-          example = "/var/run/bird/bird.ctl";
+          default = "/var/run/bird/bird.ctl";
           description = lib.mdDoc "Bird control socket path.";
         };
 
@@ -173,6 +217,12 @@ in
             description = lib.mdDoc "Traceroute's binary path.";
           };
 
+          flags = mkOption {
+            type = with types; listOf str;
+            default = [ ];
+            description = lib.mdDoc "Flags for traceroute process";
+          };
+
           rawOutput = mkOption {
             type = types.bool;
             default = false;
@@ -181,10 +231,14 @@ in
         };
 
         extraArgs = mkOption {
-          type = types.lines;
-          default = "";
+          type = with types; either lines (listOf str);
+          default = [ ];
           description = lib.mdDoc ''
             Extra parameters documented [here](https://github.com/xddxdd/bird-lg-go#proxy).
+
+            :::{.note}
+            Passing lines (plain strings) is deprecated in favour of passing lists of strings.
+            :::
           '';
         };
       };
@@ -194,6 +248,16 @@ in
   ###### implementation
 
   config = {
+
+    warnings =
+      lib.optional (cfg.frontend.enable  && builtins.isString cfg.frontend.extraArgs) ''
+        Passing strings to `services.bird-lg.frontend.extraOptions' is deprecated. Please pass a list of strings instead.
+      ''
+      ++ lib.optional (cfg.proxy.enable  && builtins.isString cfg.proxy.extraArgs) ''
+        Passing strings to `services.bird-lg.proxy.extraOptions' is deprecated. Please pass a list of strings instead.
+      ''
+    ;
+
     systemd.services = {
       bird-lg-frontend = mkIf cfg.frontend.enable {
         enable = true;
@@ -211,23 +275,8 @@ in
         };
         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}
+            ${concatStringsSep " \\\n  " (argsAttrToList frontend_args)} \
+            ${stringOrConcat " " cfg.frontend.extraArgs}
         '';
       };
 
@@ -247,12 +296,8 @@ in
         };
         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}
+            ${concatStringsSep " \\\n  " (argsAttrToList proxy_args)} \
+            ${stringOrConcat " " cfg.proxy.extraArgs}
         '';
       };
     };
@@ -266,4 +311,9 @@ in
       };
     };
   };
+
+  meta.maintainers = with lib.maintainers; [
+    e1mo
+    tchekda
+  ];
 }
diff --git a/nixos/modules/services/networking/blockbook-frontend.nix b/nixos/modules/services/networking/blockbook-frontend.nix
index ab784563e4a..46b26195d21 100644
--- a/nixos/modules/services/networking/blockbook-frontend.nix
+++ b/nixos/modules/services/networking/blockbook-frontend.nix
@@ -10,7 +10,7 @@ let
 
     options = {
 
-      enable = mkEnableOption (lib.mdDoc "blockbook-frontend application.");
+      enable = mkEnableOption (lib.mdDoc "blockbook-frontend application");
 
       package = mkOption {
         type = types.package;
diff --git a/nixos/modules/services/networking/blocky.nix b/nixos/modules/services/networking/blocky.nix
index 97144854561..30a41fa6a42 100644
--- a/nixos/modules/services/networking/blocky.nix
+++ b/nixos/modules/services/networking/blocky.nix
@@ -31,6 +31,7 @@ in
       serviceConfig = {
         DynamicUser = true;
         ExecStart = "${pkgs.blocky}/bin/blocky --config ${configFile}";
+        Restart = "on-failure";
 
         AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
diff --git a/nixos/modules/services/networking/cgit.nix b/nixos/modules/services/networking/cgit.nix
new file mode 100644
index 00000000000..672b0b030ee
--- /dev/null
+++ b/nixos/modules/services/networking/cgit.nix
@@ -0,0 +1,203 @@
+{ config, lib, pkgs, ...}:
+
+with lib;
+
+let
+  cfgs = config.services.cgit;
+
+  settingType = with types; oneOf [ bool int str ];
+
+  genAttrs' = names: f: listToAttrs (map f names);
+
+  regexEscape =
+    let
+      # taken from https://github.com/python/cpython/blob/05cb728d68a278d11466f9a6c8258d914135c96c/Lib/re.py#L251-L266
+      special = [
+        "(" ")" "[" "]" "{" "}" "?" "*" "+" "-" "|" "^" "$" "\\" "." "&" "~"
+        "#" " " "\t" "\n" "\r" "\v" "\f"
+      ];
+    in
+      replaceStrings special (map (c: "\\${c}") special);
+
+  stripLocation = cfg: removeSuffix "/" cfg.nginx.location;
+
+  regexLocation = cfg: regexEscape (stripLocation cfg);
+
+  mkFastcgiPass = cfg: ''
+    ${if cfg.nginx.location == "/" then ''
+      fastcgi_param PATH_INFO $uri;
+    '' else ''
+      fastcgi_split_path_info ^(${regexLocation cfg})(/.+)$;
+      fastcgi_param PATH_INFO $fastcgi_path_info;
+    ''
+    }fastcgi_pass unix:${config.services.fcgiwrap.socketAddress};
+  '';
+
+  cgitrcLine = name: value: "${name}=${
+    if value == true then
+      "1"
+    else if value == false then
+      "0"
+    else
+      toString value
+  }";
+
+  mkCgitrc = cfg: pkgs.writeText "cgitrc" ''
+    # global settings
+    ${concatStringsSep "\n" (
+        mapAttrsToList
+          cgitrcLine
+          ({ virtual-root = cfg.nginx.location; } // cfg.settings)
+      )
+    }
+    ${optionalString (cfg.scanPath != null) (cgitrcLine "scan-path" cfg.scanPath)}
+
+    # repository settings
+    ${concatStrings (
+        mapAttrsToList
+          (url: settings: ''
+            ${cgitrcLine "repo.url" url}
+            ${concatStringsSep "\n" (
+                mapAttrsToList (name: cgitrcLine "repo.${name}") settings
+              )
+            }
+          '')
+          cfg.repos
+      )
+    }
+
+    # extra config
+    ${cfg.extraConfig}
+  '';
+
+  mkCgitReposDir = cfg:
+    if cfg.scanPath != null then
+      cfg.scanPath
+    else
+      pkgs.runCommand "cgit-repos" {
+        preferLocalBuild = true;
+        allowSubstitutes = false;
+      } ''
+        mkdir -p "$out"
+        ${
+          concatStrings (
+            mapAttrsToList
+              (name: value: ''
+                ln -s ${escapeShellArg value.path} "$out"/${escapeShellArg name}
+              '')
+              cfg.repos
+          )
+        }
+      '';
+
+in
+{
+  options = {
+    services.cgit = mkOption {
+      description = mdDoc "Configure cgit instances.";
+      default = {};
+      type = types.attrsOf (types.submodule ({ config, ... }: {
+        options = {
+          enable = mkEnableOption (mdDoc "cgit");
+
+          package = mkPackageOptionMD pkgs "cgit" {};
+
+          nginx.virtualHost = mkOption {
+            description = mdDoc "VirtualHost to serve cgit on, defaults to the attribute name.";
+            type = types.str;
+            default = config._module.args.name;
+            example = "git.example.com";
+          };
+
+          nginx.location = mkOption {
+            description = mdDoc "Location to serve cgit under.";
+            type = types.str;
+            default = "/";
+            example = "/git/";
+          };
+
+          repos = mkOption {
+            description = mdDoc "cgit repository settings, see cgitrc(5)";
+            type = with types; attrsOf (attrsOf settingType);
+            default = {};
+            example = {
+              blah = {
+                path = "/var/lib/git/example";
+                desc = "An example repository";
+              };
+            };
+          };
+
+          scanPath = mkOption {
+            description = mdDoc "A path which will be scanned for repositories.";
+            type = types.nullOr types.path;
+            default = null;
+            example = "/var/lib/git";
+          };
+
+          settings = mkOption {
+            description = mdDoc "cgit configuration, see cgitrc(5)";
+            type = types.attrsOf settingType;
+            default = {};
+            example = literalExpression ''
+              {
+                enable-follow-links = true;
+                source-filter = "''${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py";
+              }
+            '';
+          };
+
+          extraConfig = mkOption {
+            description = mdDoc "These lines go to the end of cgitrc verbatim.";
+            type = types.lines;
+            default = "";
+          };
+        };
+      }));
+    };
+  };
+
+  config = mkIf (any (cfg: cfg.enable) (attrValues cfgs)) {
+    assertions = mapAttrsToList (vhost: cfg: {
+      assertion = !cfg.enable || (cfg.scanPath == null) != (cfg.repos == {});
+      message = "Exactly one of services.cgit.${vhost}.scanPath or services.cgit.${vhost}.repos must be set.";
+    }) cfgs;
+
+    services.fcgiwrap.enable = true;
+
+    services.nginx.enable = true;
+
+    services.nginx.virtualHosts = mkMerge (mapAttrsToList (_: cfg: {
+      ${cfg.nginx.virtualHost} = {
+        locations = (
+          genAttrs'
+            [ "cgit.css" "cgit.png" "favicon.ico" "robots.txt" ]
+            (name: nameValuePair "= ${stripLocation cfg}/${name}" {
+              extraConfig = ''
+                alias ${cfg.package}/cgit/${name};
+              '';
+            })
+        ) // {
+          "~ ${regexLocation cfg}/.+/(info/refs|git-upload-pack)" = {
+            fastcgiParams = rec {
+              SCRIPT_FILENAME = "${pkgs.git}/libexec/git-core/git-http-backend";
+              GIT_HTTP_EXPORT_ALL = "1";
+              GIT_PROJECT_ROOT = mkCgitReposDir cfg;
+              HOME = GIT_PROJECT_ROOT;
+            };
+            extraConfig = mkFastcgiPass cfg;
+          };
+          "${stripLocation cfg}/" = {
+            fastcgiParams = {
+              SCRIPT_FILENAME = "${cfg.package}/cgit/cgit.cgi";
+              QUERY_STRING = "$args";
+              HTTP_HOST = "$server_name";
+              CGIT_CONFIG = mkCgitrc cfg;
+            };
+            extraConfig = mkFastcgiPass cfg;
+          };
+        };
+      };
+    }) cfgs);
+  };
+}
diff --git a/nixos/modules/services/networking/cloudflared.nix b/nixos/modules/services/networking/cloudflared.nix
index c8fc9fafee6..b3f0e37d8e9 100644
--- a/nixos/modules/services/networking/cloudflared.nix
+++ b/nixos/modules/services/networking/cloudflared.nix
@@ -168,8 +168,7 @@ in
           inherit originRequest;
 
           credentialsFile = mkOption {
-            type = with types; nullOr str;
-            default = null;
+            type = types.str;
             description = lib.mdDoc ''
               Credential file.
 
@@ -190,8 +189,7 @@ in
           };
 
           default = mkOption {
-            type = with types; nullOr str;
-            default = null;
+            type = types.str;
             description = lib.mdDoc ''
               Catch-all service if no ingress matches.
 
@@ -262,12 +260,12 @@ in
     systemd.targets =
       mapAttrs'
         (name: tunnel:
-          nameValuePair "cloudflared-tunnel-${name}" ({
-            description = lib.mdDoc "Cloudflare tunnel '${name}' target";
+          nameValuePair "cloudflared-tunnel-${name}" {
+            description = "Cloudflare tunnel '${name}' target";
             requires = [ "cloudflared-tunnel-${name}.service" ];
             after = [ "cloudflared-tunnel-${name}.service" ];
             unitConfig.StopWhenUnneeded = true;
-          })
+          }
         )
         config.services.cloudflared.tunnels;
 
@@ -304,13 +302,14 @@ in
             mkConfigFile = pkgs.writeText "cloudflared.yml" (builtins.toJSON fullConfig);
           in
           nameValuePair "cloudflared-tunnel-${name}" ({
-            after = [ "network.target" ];
+            after = [ "network.target" "network-online.target" ];
+            wants = [ "network.target" "network-online.target" ];
             wantedBy = [ "multi-user.target" ];
             serviceConfig = {
               User = cfg.user;
               Group = cfg.group;
               ExecStart = "${cfg.package}/bin/cloudflared tunnel --config=${mkConfigFile} --no-autoupdate run";
-              Restart = "always";
+              Restart = "on-failure";
             };
           })
         )
diff --git a/nixos/modules/services/networking/consul.nix b/nixos/modules/services/networking/consul.nix
index f1c36138be3..955463b9031 100644
--- a/nixos/modules/services/networking/consul.nix
+++ b/nixos/modules/services/networking/consul.nix
@@ -199,7 +199,7 @@ in
             (filterAttrs (n: _: hasPrefix "consul.d/" n) config.environment.etc);
 
         serviceConfig = {
-          ExecStart = "@${cfg.package}/bin/consul consul agent -config-dir /etc/consul.d"
+          ExecStart = "@${lib.getExe cfg.package} consul agent -config-dir /etc/consul.d"
             + concatMapStrings (n: " -config-file ${n}") configFiles;
           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
           PermissionsStartOnly = true;
@@ -207,10 +207,10 @@ in
           Restart = "on-failure";
           TimeoutStartSec = "infinity";
         } // (optionalAttrs (cfg.leaveOnStop) {
-          ExecStop = "${cfg.package}/bin/consul leave";
+          ExecStop = "${lib.getExe cfg.package} leave";
         });
 
-        path = with pkgs; [ iproute2 gnugrep gawk consul ];
+        path = with pkgs; [ iproute2 gawk cfg.package ];
         preStart = let
           family = if cfg.forceAddrFamily == "ipv6" then
             "-6"
@@ -269,7 +269,7 @@ in
 
         serviceConfig = {
           ExecStart = ''
-            ${cfg.alerts.package}/bin/consul-alerts start \
+            ${lib.getExe cfg.alerts.package} start \
               --alert-addr=${cfg.alerts.listenAddr} \
               --consul-addr=${cfg.alerts.consulAddr} \
               ${optionalString cfg.alerts.watchChecks "--watch-checks"} \
diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix
index 4d843641f58..7caee8a8eb3 100644
--- a/nixos/modules/services/networking/ddclient.nix
+++ b/nixos/modules/services/networking/ddclient.nix
@@ -29,9 +29,9 @@ let
   configFile = if (cfg.configFile != null) then cfg.configFile else configFile';
 
   preStart = ''
-    install ${configFile} /run/${RuntimeDirectory}/ddclient.conf
+    install --mode=600 --owner=$USER ${configFile} /run/${RuntimeDirectory}/ddclient.conf
     ${lib.optionalString (cfg.configFile == null) (if (cfg.protocol == "nsupdate") then ''
-      install ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
+      install --mode=600 --owner=$USER ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
     '' else if (cfg.passwordFile != null) then ''
       "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "${cfg.passwordFile}" "/run/${RuntimeDirectory}/ddclient.conf"
     '' else ''
@@ -218,6 +218,7 @@ with lib;
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       restartTriggers = optional (cfg.configFile != null) cfg.configFile;
+      path = lib.optional (lib.hasPrefix "if," cfg.use) pkgs.iproute2;
 
       serviceConfig = {
         DynamicUser = true;
diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix
index ac5d45a65e3..8b6d3fc55f3 100644
--- a/nixos/modules/services/networking/dhcpcd.nix
+++ b/nixos/modules/services/networking/dhcpcd.nix
@@ -33,6 +33,13 @@ let
     (if !config.networking.useDHCP && enableDHCP then
       map (i: i.name) (filter (i: i.useDHCP == true) interfaces) else null);
 
+  staticIPv6Addresses = map (i: i.name) (filter (i: i.ipv6.addresses != [ ]) interfaces);
+
+  noIPv6rs = concatStringsSep "\n" (map (name: ''
+    interface ${name}
+    noipv6rs
+  '') staticIPv6Addresses);
+
   # Config file adapted from the one that ships with dhcpcd.
   dhcpcdConf = pkgs.writeText "dhcpcd.conf"
     ''
@@ -74,6 +81,11 @@ let
         noipv6
       ''}
 
+      ${optionalString (config.networking.enableIPv6 && cfg.IPv6rs == null && staticIPv6Addresses != [ ]) noIPv6rs}
+      ${optionalString (config.networking.enableIPv6 && cfg.IPv6rs == false) ''
+        noipv6rs
+      ''}
+
       ${cfg.extraConfig}
     '';
 
@@ -151,6 +163,16 @@ in
       '';
     };
 
+    networking.dhcpcd.IPv6rs = mkOption {
+      type = types.nullOr types.bool;
+      default = null;
+      description = lib.mdDoc ''
+        Force enable or disable solicitation and receipt of IPv6 Router Advertisements.
+        This is required, for example, when using a static unique local IPv6 address (ULA)
+        and global IPv6 address auto-configuration with SLAAC.
+      '';
+    };
+
     networking.dhcpcd.runHook = mkOption {
       type = types.lines;
       default = "";
diff --git a/nixos/modules/services/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix
index 0bd5e4ef553..a981a255c3e 100644
--- a/nixos/modules/services/networking/dhcpd.nix
+++ b/nixos/modules/services/networking/dhcpd.nix
@@ -218,6 +218,13 @@ in
 
     systemd.services = dhcpdService "4" cfg4 // dhcpdService "6" cfg6;
 
+    warnings = [
+      ''
+        The dhcpd4 and dhcpd6 modules will be removed from NixOS 23.11, because ISC DHCP reached its end of life.
+        See https://www.isc.org/blogs/isc-dhcp-eol/ for details.
+        Please switch to a different implementation like kea, systemd-networkd or dnsmasq.
+      ''
+    ];
   };
 
 }
diff --git a/nixos/modules/services/networking/envoy.nix b/nixos/modules/services/networking/envoy.nix
index 20cfebb7991..c68ceab9619 100644
--- a/nixos/modules/services/networking/envoy.nix
+++ b/nixos/modules/services/networking/envoy.nix
@@ -6,18 +6,29 @@ let
   cfg = config.services.envoy;
   format = pkgs.formats.json { };
   conf = format.generate "envoy.json" cfg.settings;
-  validateConfig = file:
+  validateConfig = required: file:
     pkgs.runCommand "validate-envoy-conf" { } ''
-      ${pkgs.envoy}/bin/envoy --log-level error --mode validate -c "${file}"
+      ${cfg.package}/bin/envoy --log-level error --mode validate -c "${file}" ${lib.optionalString (!required) "|| true"}
       cp "${file}" "$out"
     '';
-
 in
 
 {
   options.services.envoy = {
     enable = mkEnableOption (lib.mdDoc "Envoy reverse proxy");
 
+    package = mkPackageOptionMD pkgs "envoy" { };
+
+    requireValidConfig = mkOption {
+      type = types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        Whether a failure during config validation at build time is fatal.
+        When the config can't be checked during build time, for example when it includes
+        other files, disable this option.
+      '';
+    };
+
     settings = mkOption {
       type = format.type;
       default = { };
@@ -46,38 +57,44 @@ in
   };
 
   config = mkIf cfg.enable {
-    environment.systemPackages = [ pkgs.envoy ];
+    environment.systemPackages = [ cfg.package ];
     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;
+        ExecStart = "${cfg.package}/bin/envoy -c ${validateConfig cfg.requireValidConfig conf}";
+        CacheDirectory = [ "envoy" ];
+        LogsDirectory = [ "envoy" ];
         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";
+        # Hardening
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+        DeviceAllow = [ "" ];
+        DevicePolicy = "closed";
+        DynamicUser = true;
         LockPersonality = true;
-        RestrictNamespaces = true;
-        RestrictRealtime = true;
-        PrivateUsers = false;  # breaks CAP_NET_BIND_SERVICE
+        MemoryDenyWriteExecute = false; # at least wasmr needs WX permission
         PrivateDevices = true;
+        PrivateUsers = false; # breaks CAP_NET_BIND_SERVICE
+        ProcSubset = "pid";
         ProtectClock = true;
         ProtectControlGroups = true;
         ProtectHome = true;
+        ProtectHostname = true;
         ProtectKernelLogs = true;
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
         ProtectProc = "ptraceable";
-        ProtectHostname = true;
         ProtectSystem = "strict";
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" "AF_XDP" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallErrorNumber = "EPERM";
+        SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
         UMask = "0066";
-        SystemCallFilter = "~@clock @module @mount @reboot @swap @obsolete @cpu-emulation";
       };
     };
   };
diff --git a/nixos/modules/services/networking/firefox-syncserver.nix b/nixos/modules/services/networking/firefox-syncserver.nix
index 9733fb16d90..42924d7f699 100644
--- a/nixos/modules/services/networking/firefox-syncserver.nix
+++ b/nixos/modules/services/networking/firefox-syncserver.nix
@@ -304,6 +304,10 @@ in
         forceSSL = cfg.singleNode.enableTLS;
         locations."/" = {
           proxyPass = "http://127.0.0.1:${toString cfg.settings.port}";
+          # We need to pass the Host header that matches the original Host header. Otherwise,
+          # Hawk authentication will fail (because it assumes that the client and server see
+          # the same value of the Host header).
+          recommendedProxySettings = true;
         };
       };
     };
@@ -311,8 +315,6 @@ in
 
   meta = {
     maintainers = with lib.maintainers; [ pennae ];
-    # Don't edit the docbook xml directly, edit the md and generate it:
-    # `pandoc firefox-syncserver.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > firefox-syncserver.xml`
-    doc = ./firefox-syncserver.xml;
+    doc = ./firefox-syncserver.md;
   };
 }
diff --git a/nixos/modules/services/networking/firefox-syncserver.xml b/nixos/modules/services/networking/firefox-syncserver.xml
deleted file mode 100644
index 66c81226695..00000000000
--- a/nixos/modules/services/networking/firefox-syncserver.xml
+++ /dev/null
@@ -1,77 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-firefox-syncserver">
-  <title>Firefox Sync server</title>
-  <para>
-    A storage server for Firefox Sync that you can easily host yourself.
-  </para>
-  <section xml:id="module-services-firefox-syncserver-quickstart">
-    <title>Quickstart</title>
-    <para>
-      The absolute minimal configuration for the sync server looks like
-      this:
-    </para>
-    <programlisting language="nix">
-services.mysql.package = pkgs.mariadb;
-
-services.firefox-syncserver = {
-  enable = true;
-  secrets = builtins.toFile &quot;sync-secrets&quot; ''
-    SYNC_MASTER_SECRET=this-secret-is-actually-leaked-to-/nix/store
-  '';
-  singleNode = {
-    enable = true;
-    hostname = &quot;localhost&quot;;
-    url = &quot;http://localhost:5000&quot;;
-  };
-};
-</programlisting>
-    <para>
-      This will start a sync server that is only accessible locally.
-      Once the services is running you can navigate to
-      <literal>about:config</literal> in your Firefox profile and set
-      <literal>identity.sync.tokenserver.uri</literal> to
-      <literal>http://localhost:5000/1.0/sync/1.5</literal>. Your
-      browser will now use your local sync server for data storage.
-    </para>
-    <warning>
-      <para>
-        This configuration should never be used in production. It is not
-        encrypted and stores its secrets in a world-readable location.
-      </para>
-    </warning>
-  </section>
-  <section xml:id="module-services-firefox-syncserver-configuration">
-    <title>More detailed setup</title>
-    <para>
-      The <literal>firefox-syncserver</literal> service provides a
-      number of options to make setting up small deployment easier.
-      These are grouped under the <literal>singleNode</literal> element
-      of the option tree and allow simple configuration of the most
-      important parameters.
-    </para>
-    <para>
-      Single node setup is split into two kinds of options: those that
-      affect the sync server itself, and those that affect its
-      surroundings. Options that affect the sync server are
-      <literal>capacity</literal>, which configures how many accounts
-      may be active on this instance, and <literal>url</literal>, which
-      holds the URL under which the sync server can be accessed. The
-      <literal>url</literal> can be configured automatically when using
-      nginx.
-    </para>
-    <para>
-      Options that affect the surroundings of the sync server are
-      <literal>enableNginx</literal>, <literal>enableTLS</literal> and
-      <literal>hostnam</literal>. If <literal>enableNginx</literal> is
-      set the sync server module will automatically add an nginx virtual
-      host to the system using <literal>hostname</literal> as the domain
-      and set <literal>url</literal> accordingly. If
-      <literal>enableTLS</literal> is set the module will also enable
-      ACME certificates on the new virtual host and force all
-      connections to be made via TLS.
-    </para>
-    <para>
-      For actual deployment it is also recommended to store the
-      <literal>secrets</literal> file in a secure location.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/networking/firewall-iptables.nix b/nixos/modules/services/networking/firewall-iptables.nix
new file mode 100644
index 00000000000..63e952194d6
--- /dev/null
+++ b/nixos/modules/services/networking/firewall-iptables.nix
@@ -0,0 +1,334 @@
+/* This module enables a simple firewall.
+
+   The firewall can be customised in arbitrary ways by setting
+   ‘networking.firewall.extraCommands’.  For modularity, the firewall
+   uses several chains:
+
+   - ‘nixos-fw’ is the main chain for input packet processing.
+
+   - ‘nixos-fw-accept’ is called for accepted packets.  If you want
+   additional logging, or want to reject certain packets anyway, you
+   can insert rules at the start of this chain.
+
+   - ‘nixos-fw-log-refuse’ and ‘nixos-fw-refuse’ are called for
+   refused packets.  (The former jumps to the latter after logging
+   the packet.)  If you want additional logging, or want to accept
+   certain packets anyway, you can insert rules at the start of
+   this chain.
+
+   - ‘nixos-fw-rpfilter’ is used as the main chain in the mangle table,
+   called from the built-in ‘PREROUTING’ chain.  If the kernel
+   supports it and `cfg.checkReversePath` is set this chain will
+   perform a reverse path filter test.
+
+   - ‘nixos-drop’ is used while reloading the firewall in order to drop
+   all traffic.  Since reloading isn't implemented in an atomic way
+   this'll prevent any traffic from leaking through while reloading
+   the firewall.  However, if the reloading fails, the ‘firewall-stop’
+   script will be called which in return will effectively disable the
+   complete firewall (in the default configuration).
+
+*/
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.networking.firewall;
+
+  inherit (config.boot.kernelPackages) kernel;
+
+  kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false);
+
+  helpers = import ./helpers.nix { inherit config lib; };
+
+  writeShScript = name: text:
+    let
+      dir = pkgs.writeScriptBin name ''
+        #! ${pkgs.runtimeShell} -e
+        ${text}
+      '';
+    in
+    "${dir}/bin/${name}";
+
+  startScript = writeShScript "firewall-start" ''
+    ${helpers}
+
+    # Flush the old firewall rules.  !!! Ideally, updating the
+    # firewall would be atomic.  Apparently that's possible
+    # with iptables-restore.
+    ip46tables -D INPUT -j nixos-fw 2> /dev/null || true
+    for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do
+      ip46tables -F "$chain" 2> /dev/null || true
+      ip46tables -X "$chain" 2> /dev/null || true
+    done
+
+
+    # The "nixos-fw-accept" chain just accepts packets.
+    ip46tables -N nixos-fw-accept
+    ip46tables -A nixos-fw-accept -j ACCEPT
+
+
+    # The "nixos-fw-refuse" chain rejects or drops packets.
+    ip46tables -N nixos-fw-refuse
+
+    ${if cfg.rejectPackets then ''
+      # Send a reset for existing TCP connections that we've
+      # somehow forgotten about.  Send ICMP "port unreachable"
+      # for everything else.
+      ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset
+      ip46tables -A nixos-fw-refuse -j REJECT
+    '' else ''
+      ip46tables -A nixos-fw-refuse -j DROP
+    ''}
+
+
+    # The "nixos-fw-log-refuse" chain performs logging, then
+    # jumps to the "nixos-fw-refuse" chain.
+    ip46tables -N nixos-fw-log-refuse
+
+    ${optionalString cfg.logRefusedConnections ''
+      ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: "
+    ''}
+    ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
+      ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \
+        -j LOG --log-level info --log-prefix "refused broadcast: "
+      ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \
+        -j LOG --log-level info --log-prefix "refused multicast: "
+    ''}
+    ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
+    ${optionalString cfg.logRefusedPackets ''
+      ip46tables -A nixos-fw-log-refuse \
+        -j LOG --log-level info --log-prefix "refused packet: "
+    ''}
+    ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse
+
+
+    # The "nixos-fw" chain does the actual work.
+    ip46tables -N nixos-fw
+
+    # Clean up rpfilter rules
+    ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
+    ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true
+    ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true
+
+    ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
+      # Perform a reverse-path test to refuse spoofers
+      # For now, we just drop, as the mangle table doesn't have a log-refuse yet
+      ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true
+      ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN
+
+      # Allows this host to act as a DHCP4 client without first having to use APIPA
+      iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN
+
+      # Allows this host to act as a DHCPv4 server
+      iptables -t mangle -A nixos-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN
+
+      ${optionalString cfg.logReversePathDrops ''
+        ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
+      ''}
+      ip46tables -t mangle -A nixos-fw-rpfilter -j DROP
+
+      ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter
+    ''}
+
+    # Accept all traffic on the trusted interfaces.
+    ${flip concatMapStrings cfg.trustedInterfaces (iface: ''
+      ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept
+    '')}
+
+    # Accept packets from established or related connections.
+    ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept
+
+    # Accept connections to the allowed TCP ports.
+    ${concatStrings (mapAttrsToList (iface: cfg:
+      concatMapStrings (port:
+        ''
+          ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
+        ''
+      ) cfg.allowedTCPPorts
+    ) cfg.allInterfaces)}
+
+    # Accept connections to the allowed TCP port ranges.
+    ${concatStrings (mapAttrsToList (iface: cfg:
+      concatMapStrings (rangeAttr:
+        let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
+        ''
+          ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
+        ''
+      ) cfg.allowedTCPPortRanges
+    ) cfg.allInterfaces)}
+
+    # Accept packets on the allowed UDP ports.
+    ${concatStrings (mapAttrsToList (iface: cfg:
+      concatMapStrings (port:
+        ''
+          ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
+        ''
+      ) cfg.allowedUDPPorts
+    ) cfg.allInterfaces)}
+
+    # Accept packets on the allowed UDP port ranges.
+    ${concatStrings (mapAttrsToList (iface: cfg:
+      concatMapStrings (rangeAttr:
+        let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
+        ''
+          ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
+        ''
+      ) cfg.allowedUDPPortRanges
+    ) cfg.allInterfaces)}
+
+    # Optionally respond to ICMPv4 pings.
+    ${optionalString cfg.allowPing ''
+      iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null)
+        "-m limit ${cfg.pingLimit} "
+      }-j nixos-fw-accept
+    ''}
+
+    ${optionalString config.networking.enableIPv6 ''
+      # Accept all ICMPv6 messages except redirects and node
+      # information queries (type 139).  See RFC 4890, section
+      # 4.4.
+      ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
+      ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
+      ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
+
+      # Allow this host to act as a DHCPv6 client
+      ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept
+    ''}
+
+    ${cfg.extraCommands}
+
+    # Reject/drop everything else.
+    ip46tables -A nixos-fw -j nixos-fw-log-refuse
+
+
+    # Enable the firewall.
+    ip46tables -A INPUT -j nixos-fw
+  '';
+
+  stopScript = writeShScript "firewall-stop" ''
+    ${helpers}
+
+    # Clean up in case reload fails
+    ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
+
+    # Clean up after added ruleset
+    ip46tables -D INPUT -j nixos-fw 2>/dev/null || true
+
+    ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
+      ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true
+    ''}
+
+    ${cfg.extraStopCommands}
+  '';
+
+  reloadScript = writeShScript "firewall-reload" ''
+    ${helpers}
+
+    # Create a unique drop rule
+    ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
+    ip46tables -F nixos-drop 2>/dev/null || true
+    ip46tables -X nixos-drop 2>/dev/null || true
+    ip46tables -N nixos-drop
+    ip46tables -A nixos-drop -j DROP
+
+    # Don't allow traffic to leak out until the script has completed
+    ip46tables -A INPUT -j nixos-drop
+
+    ${cfg.extraStopCommands}
+
+    if ${startScript}; then
+      ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
+    else
+      echo "Failed to reload firewall... Stopping"
+      ${stopScript}
+      exit 1
+    fi
+  '';
+
+in
+
+{
+
+  options = {
+
+    networking.firewall = {
+      extraCommands = mkOption {
+        type = types.lines;
+        default = "";
+        example = "iptables -A INPUT -p icmp -j ACCEPT";
+        description = lib.mdDoc ''
+          Additional shell commands executed as part of the firewall
+          initialisation script.  These are executed just before the
+          final "reject" firewall rule is added, so they can be used
+          to allow packets that would otherwise be refused.
+
+          This option only works with the iptables based firewall.
+        '';
+      };
+
+      extraStopCommands = mkOption {
+        type = types.lines;
+        default = "";
+        example = "iptables -P INPUT ACCEPT";
+        description = lib.mdDoc ''
+          Additional shell commands executed as part of the firewall
+          shutdown script.  These are executed just after the removal
+          of the NixOS input rule, or if the service enters a failed
+          state.
+
+          This option only works with the iptables based firewall.
+        '';
+      };
+    };
+
+  };
+
+  # FIXME: Maybe if `enable' is false, the firewall should still be
+  # built but not started by default?
+  config = mkIf (cfg.enable && config.networking.nftables.enable == false) {
+
+    assertions = [
+      # This is approximately "checkReversePath -> kernelHasRPFilter",
+      # but the checkReversePath option can include non-boolean
+      # values.
+      {
+        assertion = cfg.checkReversePath == false || kernelHasRPFilter;
+        message = "This kernel does not support rpfilter";
+      }
+    ];
+
+    networking.firewall.checkReversePath = mkIf (!kernelHasRPFilter) (mkDefault false);
+
+    systemd.services.firewall = {
+      description = "Firewall";
+      wantedBy = [ "sysinit.target" ];
+      wants = [ "network-pre.target" ];
+      before = [ "network-pre.target" ];
+      after = [ "systemd-modules-load.service" ];
+
+      path = [ cfg.package ] ++ cfg.extraPackages;
+
+      # FIXME: this module may also try to load kernel modules, but
+      # containers don't have CAP_SYS_MODULE.  So the host system had
+      # better have all necessary modules already loaded.
+      unitConfig.ConditionCapability = "CAP_NET_ADMIN";
+      unitConfig.DefaultDependencies = false;
+
+      reloadIfChanged = true;
+
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "@${startScript} firewall-start";
+        ExecReload = "@${reloadScript} firewall-reload";
+        ExecStop = "@${stopScript} firewall-stop";
+      };
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/firewall-nftables.nix b/nixos/modules/services/networking/firewall-nftables.nix
new file mode 100644
index 00000000000..452dd97d89d
--- /dev/null
+++ b/nixos/modules/services/networking/firewall-nftables.nix
@@ -0,0 +1,179 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.networking.firewall;
+
+  ifaceSet = concatStringsSep ", " (
+    map (x: ''"${x}"'') cfg.trustedInterfaces
+  );
+
+  portsToNftSet = ports: portRanges: concatStringsSep ", " (
+    map (x: toString x) ports
+    ++ map (x: "${toString x.from}-${toString x.to}") portRanges
+  );
+
+in
+
+{
+
+  options = {
+
+    networking.firewall = {
+      extraInputRules = mkOption {
+        type = types.lines;
+        default = "";
+        example = "ip6 saddr { fc00::/7, fe80::/10 } tcp dport 24800 accept";
+        description = lib.mdDoc ''
+          Additional nftables rules to be appended to the input-allow
+          chain.
+
+          This option only works with the nftables based firewall.
+        '';
+      };
+
+      extraForwardRules = mkOption {
+        type = types.lines;
+        default = "";
+        example = "iifname wg0 accept";
+        description = lib.mdDoc ''
+          Additional nftables rules to be appended to the forward-allow
+          chain.
+
+          This option only works with the nftables based firewall.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf (cfg.enable && config.networking.nftables.enable) {
+
+    assertions = [
+      {
+        assertion = cfg.extraCommands == "";
+        message = "extraCommands is incompatible with the nftables based firewall: ${cfg.extraCommands}";
+      }
+      {
+        assertion = cfg.extraStopCommands == "";
+        message = "extraStopCommands is incompatible with the nftables based firewall: ${cfg.extraStopCommands}";
+      }
+      {
+        assertion = cfg.pingLimit == null || !(hasPrefix "--" cfg.pingLimit);
+        message = "nftables syntax like \"2/second\" should be used in networking.firewall.pingLimit";
+      }
+      {
+        assertion = config.networking.nftables.rulesetFile == null;
+        message = "networking.nftables.rulesetFile conflicts with the firewall";
+      }
+    ];
+
+    networking.nftables.ruleset = ''
+
+      table inet nixos-fw {
+
+        ${optionalString (cfg.checkReversePath != false) ''
+          chain rpfilter {
+            type filter hook prerouting priority mangle + 10; policy drop;
+
+            meta nfproto ipv4 udp sport . udp dport { 67 . 68, 68 . 67 } accept comment "DHCPv4 client/server"
+            fib saddr . mark ${optionalString (cfg.checkReversePath != "loose") ". iif"} oif exists accept
+
+            ${optionalString cfg.logReversePathDrops ''
+              log level info prefix "rpfilter drop: "
+            ''}
+
+          }
+        ''}
+
+        chain input {
+          type filter hook input priority filter; policy drop;
+
+          ${optionalString (ifaceSet != "") ''iifname { ${ifaceSet} } accept comment "trusted interfaces"''}
+
+          # Some ICMPv6 types like NDP is untracked
+          ct state vmap {
+            invalid : drop,
+            established : accept,
+            related : accept,
+            new : jump input-allow,
+            untracked: jump input-allow,
+          }
+
+          ${optionalString cfg.logRefusedConnections ''
+            tcp flags syn / fin,syn,rst,ack log level info prefix "refused connection: "
+          ''}
+          ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
+            pkttype broadcast log level info prefix "refused broadcast: "
+            pkttype multicast log level info prefix "refused multicast: "
+          ''}
+          ${optionalString cfg.logRefusedPackets ''
+            pkttype host log level info prefix "refused packet: "
+          ''}
+
+          ${optionalString cfg.rejectPackets ''
+            meta l4proto tcp reject with tcp reset
+            reject
+          ''}
+
+        }
+
+        chain input-allow {
+
+          ${concatStrings (mapAttrsToList (iface: cfg:
+            let
+              ifaceExpr = optionalString (iface != "default") "iifname ${iface}";
+              tcpSet = portsToNftSet cfg.allowedTCPPorts cfg.allowedTCPPortRanges;
+              udpSet = portsToNftSet cfg.allowedUDPPorts cfg.allowedUDPPortRanges;
+            in
+            ''
+              ${optionalString (tcpSet != "") "${ifaceExpr} tcp dport { ${tcpSet} } accept"}
+              ${optionalString (udpSet != "") "${ifaceExpr} udp dport { ${udpSet} } accept"}
+            ''
+          ) cfg.allInterfaces)}
+
+          ${optionalString cfg.allowPing ''
+            icmp type echo-request ${optionalString (cfg.pingLimit != null) "limit rate ${cfg.pingLimit}"} accept comment "allow ping"
+          ''}
+
+          icmpv6 type != { nd-redirect, 139 } accept comment "Accept all ICMPv6 messages except redirects and node information queries (type 139).  See RFC 4890, section 4.4."
+          ip6 daddr fe80::/64 udp dport 546 accept comment "DHCPv6 client"
+
+          ${cfg.extraInputRules}
+
+        }
+
+        ${optionalString cfg.filterForward ''
+          chain forward {
+            type filter hook forward priority filter; policy drop;
+
+            ct state vmap {
+              invalid : drop,
+              established : accept,
+              related : accept,
+              new : jump forward-allow,
+              untracked : jump forward-allow,
+            }
+
+          }
+
+          chain forward-allow {
+
+            icmpv6 type != { router-renumbering, 139 } accept comment "Accept all ICMPv6 messages except renumbering and node information queries (type 139).  See RFC 4890, section 4.3."
+
+            ct status dnat accept comment "allow port forward"
+
+            ${cfg.extraForwardRules}
+
+          }
+        ''}
+
+      }
+
+    '';
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/firewall.nix b/nixos/modules/services/networking/firewall.nix
index 27119dcc57c..ac02a93836b 100644
--- a/nixos/modules/services/networking/firewall.nix
+++ b/nixos/modules/services/networking/firewall.nix
@@ -1,35 +1,3 @@
-/* This module enables a simple firewall.
-
-   The firewall can be customised in arbitrary ways by setting
-   ‘networking.firewall.extraCommands’.  For modularity, the firewall
-   uses several chains:
-
-   - ‘nixos-fw’ is the main chain for input packet processing.
-
-   - ‘nixos-fw-accept’ is called for accepted packets.  If you want
-     additional logging, or want to reject certain packets anyway, you
-     can insert rules at the start of this chain.
-
-   - ‘nixos-fw-log-refuse’ and ‘nixos-fw-refuse’ are called for
-     refused packets.  (The former jumps to the latter after logging
-     the packet.)  If you want additional logging, or want to accept
-     certain packets anyway, you can insert rules at the start of
-     this chain.
-
-   - ‘nixos-fw-rpfilter’ is used as the main chain in the mangle table,
-     called from the built-in ‘PREROUTING’ chain.  If the kernel
-     supports it and `cfg.checkReversePath` is set this chain will
-     perform a reverse path filter test.
-
-   - ‘nixos-drop’ is used while reloading the firewall in order to drop
-     all traffic.  Since reloading isn't implemented in an atomic way
-     this'll prevent any traffic from leaking through while reloading
-     the firewall.  However, if the reloading fails, the ‘firewall-stop’
-     script will be called which in return will effectively disable the
-     complete firewall (in the default configuration).
-
-*/
-
 { config, lib, pkgs, ... }:
 
 with lib;
@@ -38,216 +6,6 @@ let
 
   cfg = config.networking.firewall;
 
-  inherit (config.boot.kernelPackages) kernel;
-
-  kernelHasRPFilter = ((kernel.config.isEnabled or (x: false)) "IP_NF_MATCH_RPFILTER") || (kernel.features.netfilterRPFilter or false);
-
-  helpers = import ./helpers.nix { inherit config lib; };
-
-  writeShScript = name: text: let dir = pkgs.writeScriptBin name ''
-    #! ${pkgs.runtimeShell} -e
-    ${text}
-  ''; in "${dir}/bin/${name}";
-
-  defaultInterface = { default = mapAttrs (name: value: cfg.${name}) commonOptions; };
-  allInterfaces = defaultInterface // cfg.interfaces;
-
-  startScript = writeShScript "firewall-start" ''
-    ${helpers}
-
-    # Flush the old firewall rules.  !!! Ideally, updating the
-    # firewall would be atomic.  Apparently that's possible
-    # with iptables-restore.
-    ip46tables -D INPUT -j nixos-fw 2> /dev/null || true
-    for chain in nixos-fw nixos-fw-accept nixos-fw-log-refuse nixos-fw-refuse; do
-      ip46tables -F "$chain" 2> /dev/null || true
-      ip46tables -X "$chain" 2> /dev/null || true
-    done
-
-
-    # The "nixos-fw-accept" chain just accepts packets.
-    ip46tables -N nixos-fw-accept
-    ip46tables -A nixos-fw-accept -j ACCEPT
-
-
-    # The "nixos-fw-refuse" chain rejects or drops packets.
-    ip46tables -N nixos-fw-refuse
-
-    ${if cfg.rejectPackets then ''
-      # Send a reset for existing TCP connections that we've
-      # somehow forgotten about.  Send ICMP "port unreachable"
-      # for everything else.
-      ip46tables -A nixos-fw-refuse -p tcp ! --syn -j REJECT --reject-with tcp-reset
-      ip46tables -A nixos-fw-refuse -j REJECT
-    '' else ''
-      ip46tables -A nixos-fw-refuse -j DROP
-    ''}
-
-
-    # The "nixos-fw-log-refuse" chain performs logging, then
-    # jumps to the "nixos-fw-refuse" chain.
-    ip46tables -N nixos-fw-log-refuse
-
-    ${optionalString cfg.logRefusedConnections ''
-      ip46tables -A nixos-fw-log-refuse -p tcp --syn -j LOG --log-level info --log-prefix "refused connection: "
-    ''}
-    ${optionalString (cfg.logRefusedPackets && !cfg.logRefusedUnicastsOnly) ''
-      ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type broadcast \
-        -j LOG --log-level info --log-prefix "refused broadcast: "
-      ip46tables -A nixos-fw-log-refuse -m pkttype --pkt-type multicast \
-        -j LOG --log-level info --log-prefix "refused multicast: "
-    ''}
-    ip46tables -A nixos-fw-log-refuse -m pkttype ! --pkt-type unicast -j nixos-fw-refuse
-    ${optionalString cfg.logRefusedPackets ''
-      ip46tables -A nixos-fw-log-refuse \
-        -j LOG --log-level info --log-prefix "refused packet: "
-    ''}
-    ip46tables -A nixos-fw-log-refuse -j nixos-fw-refuse
-
-
-    # The "nixos-fw" chain does the actual work.
-    ip46tables -N nixos-fw
-
-    # Clean up rpfilter rules
-    ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2> /dev/null || true
-    ip46tables -t mangle -F nixos-fw-rpfilter 2> /dev/null || true
-    ip46tables -t mangle -X nixos-fw-rpfilter 2> /dev/null || true
-
-    ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
-      # Perform a reverse-path test to refuse spoofers
-      # For now, we just drop, as the mangle table doesn't have a log-refuse yet
-      ip46tables -t mangle -N nixos-fw-rpfilter 2> /dev/null || true
-      ip46tables -t mangle -A nixos-fw-rpfilter -m rpfilter --validmark ${optionalString (cfg.checkReversePath == "loose") "--loose"} -j RETURN
-
-      # Allows this host to act as a DHCP4 client without first having to use APIPA
-      iptables -t mangle -A nixos-fw-rpfilter -p udp --sport 67 --dport 68 -j RETURN
-
-      # Allows this host to act as a DHCPv4 server
-      iptables -t mangle -A nixos-fw-rpfilter -s 0.0.0.0 -d 255.255.255.255 -p udp --sport 68 --dport 67 -j RETURN
-
-      ${optionalString cfg.logReversePathDrops ''
-        ip46tables -t mangle -A nixos-fw-rpfilter -j LOG --log-level info --log-prefix "rpfilter drop: "
-      ''}
-      ip46tables -t mangle -A nixos-fw-rpfilter -j DROP
-
-      ip46tables -t mangle -A PREROUTING -j nixos-fw-rpfilter
-    ''}
-
-    # Accept all traffic on the trusted interfaces.
-    ${flip concatMapStrings cfg.trustedInterfaces (iface: ''
-      ip46tables -A nixos-fw -i ${iface} -j nixos-fw-accept
-    '')}
-
-    # Accept packets from established or related connections.
-    ip46tables -A nixos-fw -m conntrack --ctstate ESTABLISHED,RELATED -j nixos-fw-accept
-
-    # Accept connections to the allowed TCP ports.
-    ${concatStrings (mapAttrsToList (iface: cfg:
-      concatMapStrings (port:
-        ''
-          ip46tables -A nixos-fw -p tcp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
-        ''
-      ) cfg.allowedTCPPorts
-    ) allInterfaces)}
-
-    # Accept connections to the allowed TCP port ranges.
-    ${concatStrings (mapAttrsToList (iface: cfg:
-      concatMapStrings (rangeAttr:
-        let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
-        ''
-          ip46tables -A nixos-fw -p tcp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
-        ''
-      ) cfg.allowedTCPPortRanges
-    ) allInterfaces)}
-
-    # Accept packets on the allowed UDP ports.
-    ${concatStrings (mapAttrsToList (iface: cfg:
-      concatMapStrings (port:
-        ''
-          ip46tables -A nixos-fw -p udp --dport ${toString port} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
-        ''
-      ) cfg.allowedUDPPorts
-    ) allInterfaces)}
-
-    # Accept packets on the allowed UDP port ranges.
-    ${concatStrings (mapAttrsToList (iface: cfg:
-      concatMapStrings (rangeAttr:
-        let range = toString rangeAttr.from + ":" + toString rangeAttr.to; in
-        ''
-          ip46tables -A nixos-fw -p udp --dport ${range} -j nixos-fw-accept ${optionalString (iface != "default") "-i ${iface}"}
-        ''
-      ) cfg.allowedUDPPortRanges
-    ) allInterfaces)}
-
-    # Optionally respond to ICMPv4 pings.
-    ${optionalString cfg.allowPing ''
-      iptables -w -A nixos-fw -p icmp --icmp-type echo-request ${optionalString (cfg.pingLimit != null)
-        "-m limit ${cfg.pingLimit} "
-      }-j nixos-fw-accept
-    ''}
-
-    ${optionalString config.networking.enableIPv6 ''
-      # Accept all ICMPv6 messages except redirects and node
-      # information queries (type 139).  See RFC 4890, section
-      # 4.4.
-      ip6tables -A nixos-fw -p icmpv6 --icmpv6-type redirect -j DROP
-      ip6tables -A nixos-fw -p icmpv6 --icmpv6-type 139 -j DROP
-      ip6tables -A nixos-fw -p icmpv6 -j nixos-fw-accept
-
-      # Allow this host to act as a DHCPv6 client
-      ip6tables -A nixos-fw -d fe80::/64 -p udp --dport 546 -j nixos-fw-accept
-    ''}
-
-    ${cfg.extraCommands}
-
-    # Reject/drop everything else.
-    ip46tables -A nixos-fw -j nixos-fw-log-refuse
-
-
-    # Enable the firewall.
-    ip46tables -A INPUT -j nixos-fw
-  '';
-
-  stopScript = writeShScript "firewall-stop" ''
-    ${helpers}
-
-    # Clean up in case reload fails
-    ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
-
-    # Clean up after added ruleset
-    ip46tables -D INPUT -j nixos-fw 2>/dev/null || true
-
-    ${optionalString (kernelHasRPFilter && (cfg.checkReversePath != false)) ''
-      ip46tables -t mangle -D PREROUTING -j nixos-fw-rpfilter 2>/dev/null || true
-    ''}
-
-    ${cfg.extraStopCommands}
-  '';
-
-  reloadScript = writeShScript "firewall-reload" ''
-    ${helpers}
-
-    # Create a unique drop rule
-    ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
-    ip46tables -F nixos-drop 2>/dev/null || true
-    ip46tables -X nixos-drop 2>/dev/null || true
-    ip46tables -N nixos-drop
-    ip46tables -A nixos-drop -j DROP
-
-    # Don't allow traffic to leak out until the script has completed
-    ip46tables -A INPUT -j nixos-drop
-
-    ${cfg.extraStopCommands}
-
-    if ${startScript}; then
-      ip46tables -D INPUT -j nixos-drop 2>/dev/null || true
-    else
-      echo "Failed to reload firewall... Stopping"
-      ${stopScript}
-      exit 1
-    fi
-  '';
-
   canonicalizePortList =
     ports: lib.unique (builtins.sort builtins.lessThan ports);
 
@@ -257,22 +15,20 @@ let
       default = [ ];
       apply = canonicalizePortList;
       example = [ 22 80 ];
-      description =
-        lib.mdDoc ''
-          List of TCP ports on which incoming connections are
-          accepted.
-        '';
+      description = lib.mdDoc ''
+        List of TCP ports on which incoming connections are
+        accepted.
+      '';
     };
 
     allowedTCPPortRanges = mkOption {
       type = types.listOf (types.attrsOf types.port);
       default = [ ];
-      example = [ { from = 8999; to = 9003; } ];
-      description =
-        lib.mdDoc ''
-          A range of TCP ports on which incoming connections are
-          accepted.
-        '';
+      example = [{ from = 8999; to = 9003; }];
+      description = lib.mdDoc ''
+        A range of TCP ports on which incoming connections are
+        accepted.
+      '';
     };
 
     allowedUDPPorts = mkOption {
@@ -280,20 +36,18 @@ let
       default = [ ];
       apply = canonicalizePortList;
       example = [ 53 ];
-      description =
-        lib.mdDoc ''
-          List of open UDP ports.
-        '';
+      description = lib.mdDoc ''
+        List of open UDP ports.
+      '';
     };
 
     allowedUDPPortRanges = mkOption {
       type = types.listOf (types.attrsOf types.port);
       default = [ ];
-      example = [ { from = 60000; to = 61000; } ];
-      description =
-        lib.mdDoc ''
-          Range of open UDP ports.
-        '';
+      example = [{ from = 60000; to = 61000; }];
+      description = lib.mdDoc ''
+        Range of open UDP ports.
+      '';
     };
   };
 
@@ -301,240 +55,226 @@ in
 
 {
 
-  ###### interface
-
   options = {
 
     networking.firewall = {
       enable = mkOption {
         type = types.bool;
         default = true;
-        description =
-          lib.mdDoc ''
-            Whether to enable the firewall.  This is a simple stateful
-            firewall that blocks connection attempts to unauthorised TCP
-            or UDP ports on this machine.  It does not affect packet
-            forwarding.
-          '';
+        description = lib.mdDoc ''
+          Whether to enable the firewall.  This is a simple stateful
+          firewall that blocks connection attempts to unauthorised TCP
+          or UDP ports on this machine.
+        '';
       };
 
       package = mkOption {
         type = types.package;
-        default = pkgs.iptables;
-        defaultText = literalExpression "pkgs.iptables";
+        default = if config.networking.nftables.enable then pkgs.nftables else pkgs.iptables;
+        defaultText = literalExpression ''if config.networking.nftables.enable then "pkgs.nftables" else "pkgs.iptables"'';
         example = literalExpression "pkgs.iptables-legacy";
-        description =
-          lib.mdDoc ''
-            The iptables package to use for running the firewall service.
-          '';
+        description = lib.mdDoc ''
+          The package to use for running the firewall service.
+        '';
       };
 
       logRefusedConnections = mkOption {
         type = types.bool;
         default = true;
-        description =
-          lib.mdDoc ''
-            Whether to log rejected or dropped incoming connections.
-            Note: The logs are found in the kernel logs, i.e. dmesg
-            or journalctl -k.
-          '';
+        description = lib.mdDoc ''
+          Whether to log rejected or dropped incoming connections.
+          Note: The logs are found in the kernel logs, i.e. dmesg
+          or journalctl -k.
+        '';
       };
 
       logRefusedPackets = mkOption {
         type = types.bool;
         default = false;
-        description =
-          lib.mdDoc ''
-            Whether to log all rejected or dropped incoming packets.
-            This tends to give a lot of log messages, so it's mostly
-            useful for debugging.
-            Note: The logs are found in the kernel logs, i.e. dmesg
-            or journalctl -k.
-          '';
+        description = lib.mdDoc ''
+          Whether to log all rejected or dropped incoming packets.
+          This tends to give a lot of log messages, so it's mostly
+          useful for debugging.
+          Note: The logs are found in the kernel logs, i.e. dmesg
+          or journalctl -k.
+        '';
       };
 
       logRefusedUnicastsOnly = mkOption {
         type = types.bool;
         default = true;
-        description =
-          lib.mdDoc ''
-            If {option}`networking.firewall.logRefusedPackets`
-            and this option are enabled, then only log packets
-            specifically directed at this machine, i.e., not broadcasts
-            or multicasts.
-          '';
+        description = lib.mdDoc ''
+          If {option}`networking.firewall.logRefusedPackets`
+          and this option are enabled, then only log packets
+          specifically directed at this machine, i.e., not broadcasts
+          or multicasts.
+        '';
       };
 
       rejectPackets = mkOption {
         type = types.bool;
         default = false;
-        description =
-          lib.mdDoc ''
-            If set, refused packets are rejected rather than dropped
-            (ignored).  This means that an ICMP "port unreachable" error
-            message is sent back to the client (or a TCP RST packet in
-            case of an existing connection).  Rejecting packets makes
-            port scanning somewhat easier.
-          '';
+        description = lib.mdDoc ''
+          If set, refused packets are rejected rather than dropped
+          (ignored).  This means that an ICMP "port unreachable" error
+          message is sent back to the client (or a TCP RST packet in
+          case of an existing connection).  Rejecting packets makes
+          port scanning somewhat easier.
+        '';
       };
 
       trustedInterfaces = mkOption {
         type = types.listOf types.str;
         default = [ ];
         example = [ "enp0s2" ];
-        description =
-          lib.mdDoc ''
-            Traffic coming in from these interfaces will be accepted
-            unconditionally.  Traffic from the loopback (lo) interface
-            will always be accepted.
-          '';
+        description = lib.mdDoc ''
+          Traffic coming in from these interfaces will be accepted
+          unconditionally.  Traffic from the loopback (lo) interface
+          will always be accepted.
+        '';
       };
 
       allowPing = mkOption {
         type = types.bool;
         default = true;
-        description =
-          lib.mdDoc ''
-            Whether to respond to incoming ICMPv4 echo requests
-            ("pings").  ICMPv6 pings are always allowed because the
-            larger address space of IPv6 makes network scanning much
-            less effective.
-          '';
+        description = lib.mdDoc ''
+          Whether to respond to incoming ICMPv4 echo requests
+          ("pings").  ICMPv6 pings are always allowed because the
+          larger address space of IPv6 makes network scanning much
+          less effective.
+        '';
       };
 
       pingLimit = mkOption {
         type = types.nullOr (types.separatedString " ");
         default = null;
         example = "--limit 1/minute --limit-burst 5";
-        description =
-          lib.mdDoc ''
-            If pings are allowed, this allows setting rate limits
-            on them.  If non-null, this option should be in the form of
-            flags like "--limit 1/minute --limit-burst 5"
-          '';
+        description = lib.mdDoc ''
+          If pings are allowed, this allows setting rate limits on them.
+
+          For the iptables based firewall, it should be set like
+          "--limit 1/minute --limit-burst 5".
+
+          For the nftables based firewall, it should be set like
+          "2/second" or "1/minute burst 5 packets".
+        '';
       };
 
       checkReversePath = mkOption {
-        type = types.either types.bool (types.enum ["strict" "loose"]);
-        default = kernelHasRPFilter;
-        defaultText = literalMD "`true` if supported by the chosen kernel";
+        type = types.either types.bool (types.enum [ "strict" "loose" ]);
+        default = true;
+        defaultText = literalMD "`true` except if the iptables based firewall is in use and the kernel lacks rpfilter support";
         example = "loose";
-        description =
-          lib.mdDoc ''
-            Performs a reverse path filter test on a packet.  If a reply
-            to the packet would not be sent via the same interface that
-            the packet arrived on, it is refused.
-
-            If using asymmetric routing or other complicated routing, set
-            this option to loose mode or disable it and setup your own
-            counter-measures.
-
-            This option can be either true (or "strict"), "loose" (only
-            drop the packet if the source address is not reachable via any
-            interface) or false.  Defaults to the value of
-            kernelHasRPFilter.
-          '';
+        description = lib.mdDoc ''
+          Performs a reverse path filter test on a packet.  If a reply
+          to the packet would not be sent via the same interface that
+          the packet arrived on, it is refused.
+
+          If using asymmetric routing or other complicated routing, set
+          this option to loose mode or disable it and setup your own
+          counter-measures.
+
+          This option can be either true (or "strict"), "loose" (only
+          drop the packet if the source address is not reachable via any
+          interface) or false.
+        '';
       };
 
       logReversePathDrops = mkOption {
         type = types.bool;
         default = false;
-        description =
-          lib.mdDoc ''
-            Logs dropped packets failing the reverse path filter test if
-            the option networking.firewall.checkReversePath is enabled.
-          '';
+        description = lib.mdDoc ''
+          Logs dropped packets failing the reverse path filter test if
+          the option networking.firewall.checkReversePath is enabled.
+        '';
+      };
+
+      filterForward = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Enable filtering in IP forwarding.
+
+          This option only works with the nftables based firewall.
+        '';
       };
 
       connectionTrackingModules = mkOption {
         type = types.listOf types.str;
         default = [ ];
         example = [ "ftp" "irc" "sane" "sip" "tftp" "amanda" "h323" "netbios_sn" "pptp" "snmp" ];
-        description =
-          lib.mdDoc ''
-            List of connection-tracking helpers that are auto-loaded.
-            The complete list of possible values is given in the example.
-
-            As helpers can pose as a security risk, it is advised to
-            set this to an empty list and disable the setting
-            networking.firewall.autoLoadConntrackHelpers unless you
-            know what you are doing. Connection tracking is disabled
-            by default.
-
-            Loading of helpers is recommended to be done through the
-            CT target.  More info:
-            https://home.regit.org/netfilter-en/secure-use-of-helpers/
-          '';
+        description = lib.mdDoc ''
+          List of connection-tracking helpers that are auto-loaded.
+          The complete list of possible values is given in the example.
+
+          As helpers can pose as a security risk, it is advised to
+          set this to an empty list and disable the setting
+          networking.firewall.autoLoadConntrackHelpers unless you
+          know what you are doing. Connection tracking is disabled
+          by default.
+
+          Loading of helpers is recommended to be done through the
+          CT target.  More info:
+          https://home.regit.org/netfilter-en/secure-use-of-helpers/
+        '';
       };
 
       autoLoadConntrackHelpers = mkOption {
         type = types.bool;
         default = false;
-        description =
-          lib.mdDoc ''
-            Whether to auto-load connection-tracking helpers.
-            See the description at networking.firewall.connectionTrackingModules
-
-            (needs kernel 3.5+)
-          '';
-      };
+        description = lib.mdDoc ''
+          Whether to auto-load connection-tracking helpers.
+          See the description at networking.firewall.connectionTrackingModules
 
-      extraCommands = mkOption {
-        type = types.lines;
-        default = "";
-        example = "iptables -A INPUT -p icmp -j ACCEPT";
-        description =
-          lib.mdDoc ''
-            Additional shell commands executed as part of the firewall
-            initialisation script.  These are executed just before the
-            final "reject" firewall rule is added, so they can be used
-            to allow packets that would otherwise be refused.
-          '';
+          (needs kernel 3.5+)
+        '';
       };
 
       extraPackages = mkOption {
         type = types.listOf types.package;
         default = [ ];
         example = literalExpression "[ pkgs.ipset ]";
-        description =
-          lib.mdDoc ''
-            Additional packages to be included in the environment of the system
-            as well as the path of networking.firewall.extraCommands.
-          '';
-      };
-
-      extraStopCommands = mkOption {
-        type = types.lines;
-        default = "";
-        example = "iptables -P INPUT ACCEPT";
-        description =
-          lib.mdDoc ''
-            Additional shell commands executed as part of the firewall
-            shutdown script.  These are executed just after the removal
-            of the NixOS input rule, or if the service enters a failed
-            state.
-          '';
+        description = lib.mdDoc ''
+          Additional packages to be included in the environment of the system
+          as well as the path of networking.firewall.extraCommands.
+        '';
       };
 
       interfaces = mkOption {
         default = { };
-        type = with types; attrsOf (submodule [ { options = commonOptions; } ]);
-        description =
-          lib.mdDoc ''
-            Interface-specific open ports.
-          '';
+        type = with types; attrsOf (submodule [{ options = commonOptions; }]);
+        description = lib.mdDoc ''
+          Interface-specific open ports.
+        '';
+      };
+
+      allInterfaces = mkOption {
+        internal = true;
+        visible = false;
+        default = { default = mapAttrs (name: value: cfg.${name}) commonOptions; } // cfg.interfaces;
+        type = with types; attrsOf (submodule [{ options = commonOptions; }]);
+        description = lib.mdDoc ''
+          All open ports.
+        '';
       };
     } // commonOptions;
 
   };
 
 
-  ###### implementation
-
-  # FIXME: Maybe if `enable' is false, the firewall should still be
-  # built but not started by default?
   config = mkIf cfg.enable {
 
+    assertions = [
+      {
+        assertion = cfg.filterForward -> config.networking.nftables.enable;
+        message = "filterForward only works with the nftables based firewall";
+      }
+      {
+        assertion = cfg.autoLoadConntrackHelpers -> lib.versionOlder config.boot.kernelPackages.kernel.version "6";
+        message = "conntrack helper autoloading has been removed from kernel 6.0 and newer";
+      }
+    ];
+
     networking.firewall.trustedInterfaces = [ "lo" ];
 
     environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
@@ -545,40 +285,6 @@ in
       options nf_conntrack nf_conntrack_helper=1
     '';
 
-    assertions = [
-      # This is approximately "checkReversePath -> kernelHasRPFilter",
-      # but the checkReversePath option can include non-boolean
-      # values.
-      { assertion = cfg.checkReversePath == false || kernelHasRPFilter;
-        message = "This kernel does not support rpfilter"; }
-    ];
-
-    systemd.services.firewall = {
-      description = "Firewall";
-      wantedBy = [ "sysinit.target" ];
-      wants = [ "network-pre.target" ];
-      before = [ "network-pre.target" ];
-      after = [ "systemd-modules-load.service" ];
-
-      path = [ cfg.package ] ++ cfg.extraPackages;
-
-      # FIXME: this module may also try to load kernel modules, but
-      # containers don't have CAP_SYS_MODULE.  So the host system had
-      # better have all necessary modules already loaded.
-      unitConfig.ConditionCapability = "CAP_NET_ADMIN";
-      unitConfig.DefaultDependencies = false;
-
-      reloadIfChanged = true;
-
-      serviceConfig = {
-        Type = "oneshot";
-        RemainAfterExit = true;
-        ExecStart = "@${startScript} firewall-start";
-        ExecReload = "@${reloadScript} firewall-reload";
-        ExecStop = "@${stopScript} firewall-stop";
-      };
-    };
-
   };
 
 }
diff --git a/nixos/modules/services/networking/gnunet.nix b/nixos/modules/services/networking/gnunet.nix
index 9d1c9746f72..301fe021b0a 100644
--- a/nixos/modules/services/networking/gnunet.nix
+++ b/nixos/modules/services/networking/gnunet.nix
@@ -124,8 +124,8 @@ in
         type = types.lines;
         default = "";
         description = lib.mdDoc ''
-          Additional options that will be copied verbatim in `gnunet.conf'.
-          See `gnunet.conf(5)' for details.
+          Additional options that will be copied verbatim in `gnunet.conf`.
+          See {manpage}`gnunet.conf(5)` for details.
         '';
       };
     };
diff --git a/nixos/modules/services/networking/go-neb.nix b/nixos/modules/services/networking/go-neb.nix
index 8c04542c47c..b65bb5f548e 100644
--- a/nixos/modules/services/networking/go-neb.nix
+++ b/nixos/modules/services/networking/go-neb.nix
@@ -60,13 +60,12 @@ in {
 
       serviceConfig = {
         ExecStartPre = lib.optional (cfg.secretFile != null)
-          (pkgs.writeShellScript "pre-start" ''
+          ("+" + pkgs.writeShellScript "pre-start" ''
             umask 077
             export $(xargs < ${cfg.secretFile})
             ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > ${finalConfigFile}
             chown go-neb ${finalConfigFile}
           '');
-        PermissionsStartOnly = true;
         RuntimeDirectory = "go-neb";
         ExecStart = "${pkgs.go-neb}/bin/go-neb";
         User = "go-neb";
diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix
index 29b632ff5d2..d2851e72a0d 100644
--- a/nixos/modules/services/networking/headscale.nix
+++ b/nixos/modules/services/networking/headscale.nix
@@ -1,15 +1,18 @@
-{ config, lib, pkgs, ... }:
-with lib;
-let
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+with lib; let
   cfg = config.services.headscale;
 
   dataDir = "/var/lib/headscale";
   runDir = "/run/headscale";
 
-  settingsFormat = pkgs.formats.yaml { };
+  settingsFormat = pkgs.formats.yaml {};
   configFile = settingsFormat.generate "headscale.yaml" cfg.settings;
-in
-{
+in {
   options = {
     services.headscale = {
       enable = mkEnableOption (lib.mdDoc "headscale, Open Source coordination server for Tailscale");
@@ -51,15 +54,6 @@ in
         '';
       };
 
-      serverUrl = mkOption {
-        type = types.str;
-        default = "http://127.0.0.1:8080";
-        description = lib.mdDoc ''
-          The url clients will connect to.
-        '';
-        example = "https://myheadscale.example.com:443";
-      };
-
       address = mkOption {
         type = types.str;
         default = "127.0.0.1";
@@ -78,337 +72,383 @@ in
         example = 443;
       };
 
-      privateKeyFile = mkOption {
-        type = types.path;
-        default = "${dataDir}/private.key";
-        description = lib.mdDoc ''
-          Path to private key file, generated automatically if it does not exist.
-        '';
-      };
-
-      derp = {
-        urls = mkOption {
-          type = types.listOf types.str;
-          default = [ "https://controlplane.tailscale.com/derpmap/default" ];
-          description = lib.mdDoc ''
-            List of urls containing DERP maps.
-            See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
-          '';
-        };
-
-        paths = mkOption {
-          type = types.listOf types.path;
-          default = [ ];
-          description = lib.mdDoc ''
-            List of file paths containing DERP maps.
-            See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
-          '';
-        };
-
-
-        autoUpdate = mkOption {
-          type = types.bool;
-          default = true;
-          description = lib.mdDoc ''
-            Whether to automatically update DERP maps on a set frequency.
-          '';
-          example = false;
-        };
-
-        updateFrequency = mkOption {
-          type = types.str;
-          default = "24h";
-          description = lib.mdDoc ''
-            Frequency to update DERP maps.
-          '';
-          example = "5m";
-        };
-
-      };
-
-      ephemeralNodeInactivityTimeout = mkOption {
-        type = types.str;
-        default = "30m";
-        description = lib.mdDoc ''
-          Time before an inactive ephemeral node is deleted.
-        '';
-        example = "5m";
-      };
-
-      database = {
-        type = mkOption {
-          type = types.enum [ "sqlite3" "postgres" ];
-          example = "postgres";
-          default = "sqlite3";
-          description = lib.mdDoc "Database engine to use.";
-        };
-
-        host = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "127.0.0.1";
-          description = lib.mdDoc "Database host address.";
-        };
-
-        port = mkOption {
-          type = types.nullOr types.port;
-          default = null;
-          example = 3306;
-          description = lib.mdDoc "Database host port.";
-        };
-
-        name = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "headscale";
-          description = lib.mdDoc "Database name.";
-        };
-
-        user = mkOption {
-          type = types.nullOr types.str;
-          default = null;
-          example = "headscale";
-          description = lib.mdDoc "Database user.";
-        };
-
-        passwordFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
-          example = "/run/keys/headscale-dbpassword";
-          description = lib.mdDoc ''
-            A file containing the password corresponding to
-            {option}`database.user`.
-          '';
-        };
-
-        path = mkOption {
-          type = types.nullOr types.str;
-          default = "${dataDir}/db.sqlite";
-          description = lib.mdDoc "Path to the sqlite3 database file.";
-        };
-      };
-
-      logLevel = mkOption {
-        type = types.str;
-        default = "info";
-        description = lib.mdDoc ''
-          headscale log level.
-        '';
-        example = "debug";
-      };
-
-      dns = {
-        nameservers = mkOption {
-          type = types.listOf types.str;
-          default = [ "1.1.1.1" ];
-          description = lib.mdDoc ''
-            List of nameservers to pass to Tailscale clients.
-          '';
-        };
-
-        domains = mkOption {
-          type = types.listOf types.str;
-          default = [ ];
-          description = lib.mdDoc ''
-            Search domains to inject to Tailscale clients.
-          '';
-          example = [ "mydomain.internal" ];
-        };
-
-        magicDns = mkOption {
-          type = types.bool;
-          default = true;
-          description = lib.mdDoc ''
-            Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
-            Only works if there is at least a nameserver defined.
-          '';
-          example = false;
-        };
-
-        baseDomain = mkOption {
-          type = types.str;
-          default = "";
-          description = lib.mdDoc ''
-            Defines the base domain to create the hostnames for MagicDNS.
-            {option}`baseDomain` must be a FQDNs, without the trailing dot.
-            The FQDN of the hosts will be
-            `hostname.namespace.base_domain` (e.g.
-            `myhost.mynamespace.example.com`).
-          '';
-        };
-      };
-
-      openIdConnect = {
-        issuer = mkOption {
-          type = types.str;
-          default = "";
-          description = lib.mdDoc ''
-            URL to OpenID issuer.
-          '';
-          example = "https://openid.example.com";
-        };
-
-        clientId = mkOption {
-          type = types.str;
-          default = "";
-          description = lib.mdDoc ''
-            OpenID Connect client ID.
-          '';
-        };
-
-        clientSecretFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
-          description = lib.mdDoc ''
-            Path to OpenID Connect client secret file.
-          '';
-        };
-
-        domainMap = mkOption {
-          type = types.attrsOf types.str;
-          default = { };
-          description = lib.mdDoc ''
-            Domain map is used to map incoming users (by their email) to
-            a namespace. The key can be a string, or regex.
-          '';
-          example = {
-            ".*" = "default-namespace";
-          };
-        };
-
-      };
-
-      tls = {
-        letsencrypt = {
-          hostname = mkOption {
-            type = types.nullOr types.str;
-            default = "";
-            description = lib.mdDoc ''
-              Domain name to request a TLS certificate for.
-            '';
-          };
-          challengeType = mkOption {
-            type = types.enum [ "TLS-ALPN-01" "HTTP-01" ];
-            default = "HTTP-01";
-            description = lib.mdDoc ''
-              Type of ACME challenge to use, currently supported types:
-              `HTTP-01` or `TLS-ALPN-01`.
-            '';
-          };
-          httpListen = mkOption {
-            type = types.nullOr types.str;
-            default = ":http";
-            description = lib.mdDoc ''
-              When HTTP-01 challenge is chosen, letsencrypt must set up a
-              verification endpoint, and it will be listening on:
-              `:http = port 80`.
-            '';
-          };
-        };
-
-        certFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
-          description = lib.mdDoc ''
-            Path to already created certificate.
-          '';
-        };
-        keyFile = mkOption {
-          type = types.nullOr types.path;
-          default = null;
-          description = lib.mdDoc ''
-            Path to key for already created certificate.
-          '';
-        };
-      };
-
-      aclPolicyFile = mkOption {
-        type = types.nullOr types.path;
-        default = null;
-        description = lib.mdDoc ''
-          Path to a file containing ACL policies.
-        '';
-      };
-
       settings = mkOption {
-        type = settingsFormat.type;
-        default = { };
         description = lib.mdDoc ''
           Overrides to {file}`config.yaml` as a Nix attribute set.
-          This option is ideal for overriding settings not exposed as Nix options.
           Check the [example config](https://github.com/juanfont/headscale/blob/main/config-example.yaml)
           for possible options.
         '';
+        type = types.submodule {
+          freeformType = settingsFormat.type;
+
+          options = {
+            server_url = mkOption {
+              type = types.str;
+              default = "http://127.0.0.1:8080";
+              description = lib.mdDoc ''
+                The url clients will connect to.
+              '';
+              example = "https://myheadscale.example.com:443";
+            };
+
+            private_key_path = mkOption {
+              type = types.path;
+              default = "${dataDir}/private.key";
+              description = lib.mdDoc ''
+                Path to private key file, generated automatically if it does not exist.
+              '';
+            };
+
+            noise.private_key_path = mkOption {
+              type = types.path;
+              default = "${dataDir}/noise_private.key";
+              description = lib.mdDoc ''
+                Path to noise private key file, generated automatically if it does not exist.
+              '';
+            };
+
+            derp = {
+              urls = mkOption {
+                type = types.listOf types.str;
+                default = ["https://controlplane.tailscale.com/derpmap/default"];
+                description = lib.mdDoc ''
+                  List of urls containing DERP maps.
+                  See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
+                '';
+              };
+
+              paths = mkOption {
+                type = types.listOf types.path;
+                default = [];
+                description = lib.mdDoc ''
+                  List of file paths containing DERP maps.
+                  See [How Tailscale works](https://tailscale.com/blog/how-tailscale-works/) for more information on DERP maps.
+                '';
+              };
+
+              auto_update_enable = mkOption {
+                type = types.bool;
+                default = true;
+                description = lib.mdDoc ''
+                  Whether to automatically update DERP maps on a set frequency.
+                '';
+                example = false;
+              };
+
+              update_frequency = mkOption {
+                type = types.str;
+                default = "24h";
+                description = lib.mdDoc ''
+                  Frequency to update DERP maps.
+                '';
+                example = "5m";
+              };
+            };
+
+            ephemeral_node_inactivity_timeout = mkOption {
+              type = types.str;
+              default = "30m";
+              description = lib.mdDoc ''
+                Time before an inactive ephemeral node is deleted.
+              '';
+              example = "5m";
+            };
+
+            db_type = mkOption {
+              type = types.enum ["sqlite3" "postgres"];
+              example = "postgres";
+              default = "sqlite3";
+              description = lib.mdDoc "Database engine to use.";
+            };
+
+            db_host = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "127.0.0.1";
+              description = lib.mdDoc "Database host address.";
+            };
+
+            db_port = mkOption {
+              type = types.nullOr types.port;
+              default = null;
+              example = 3306;
+              description = lib.mdDoc "Database host port.";
+            };
+
+            db_name = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "headscale";
+              description = lib.mdDoc "Database name.";
+            };
+
+            db_user = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              example = "headscale";
+              description = lib.mdDoc "Database user.";
+            };
+
+            db_password_file = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              example = "/run/keys/headscale-dbpassword";
+              description = lib.mdDoc ''
+                A file containing the password corresponding to
+                {option}`database.user`.
+              '';
+            };
+
+            db_path = mkOption {
+              type = types.nullOr types.str;
+              default = "${dataDir}/db.sqlite";
+              description = lib.mdDoc "Path to the sqlite3 database file.";
+            };
+
+            log.level = mkOption {
+              type = types.str;
+              default = "info";
+              description = lib.mdDoc ''
+                headscale log level.
+              '';
+              example = "debug";
+            };
+
+            log.format = mkOption {
+              type = types.str;
+              default = "text";
+              description = lib.mdDoc ''
+                headscale log format.
+              '';
+              example = "json";
+            };
+
+            dns_config = {
+              nameservers = mkOption {
+                type = types.listOf types.str;
+                default = ["1.1.1.1"];
+                description = lib.mdDoc ''
+                  List of nameservers to pass to Tailscale clients.
+                '';
+              };
+
+              override_local_dns = mkOption {
+                type = types.bool;
+                default = false;
+                description = lib.mdDoc ''
+                  Whether to use [Override local DNS](https://tailscale.com/kb/1054/dns/).
+                '';
+                example = true;
+              };
+
+              domains = mkOption {
+                type = types.listOf types.str;
+                default = [];
+                description = lib.mdDoc ''
+                  Search domains to inject to Tailscale clients.
+                '';
+                example = ["mydomain.internal"];
+              };
+
+              magic_dns = mkOption {
+                type = types.bool;
+                default = true;
+                description = lib.mdDoc ''
+                  Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
+                  Only works if there is at least a nameserver defined.
+                '';
+                example = false;
+              };
+
+              base_domain = mkOption {
+                type = types.str;
+                default = "";
+                description = lib.mdDoc ''
+                  Defines the base domain to create the hostnames for MagicDNS.
+                  {option}`baseDomain` must be a FQDNs, without the trailing dot.
+                  The FQDN of the hosts will be
+                  `hostname.namespace.base_domain` (e.g.
+                  `myhost.mynamespace.example.com`).
+                '';
+              };
+            };
+
+            oidc = {
+              issuer = mkOption {
+                type = types.str;
+                default = "";
+                description = lib.mdDoc ''
+                  URL to OpenID issuer.
+                '';
+                example = "https://openid.example.com";
+              };
+
+              client_id = mkOption {
+                type = types.str;
+                default = "";
+                description = lib.mdDoc ''
+                  OpenID Connect client ID.
+                '';
+              };
+
+              client_secret_path = mkOption {
+                type = types.nullOr types.path;
+                default = null;
+                description = lib.mdDoc ''
+                  Path to OpenID Connect client secret file. Expands environment variables in format ''${VAR}.
+                '';
+              };
+
+              scope = mkOption {
+                type = types.listOf types.str;
+                default = ["openid" "profile" "email"];
+                description = lib.mdDoc ''
+                  Scopes used in the OIDC flow.
+                '';
+              };
+
+              extra_params = mkOption {
+                type = types.attrsOf types.str;
+                default = { };
+                description = lib.mdDoc ''
+                  Custom query parameters to send with the Authorize Endpoint request.
+                '';
+                example = {
+                  domain_hint = "example.com";
+                };
+              };
+
+              allowed_domains = mkOption {
+                type = types.listOf types.str;
+                default = [ ];
+                description = lib.mdDoc ''
+                  Allowed principal domains. if an authenticated user's domain
+                  is not in this list authentication request will be rejected.
+                '';
+                example = [ "example.com" ];
+              };
+
+              allowed_users = mkOption {
+                type = types.listOf types.str;
+                default = [ ];
+                description = lib.mdDoc ''
+                  Users allowed to authenticate even if not in allowedDomains.
+                '';
+                example = [ "alice@example.com" ];
+              };
+
+              strip_email_domain = mkOption {
+                type = types.bool;
+                default = true;
+                description = lib.mdDoc ''
+                  Whether the domain part of the email address should be removed when generating namespaces.
+                '';
+              };
+            };
+
+            tls_letsencrypt_hostname = mkOption {
+              type = types.nullOr types.str;
+              default = "";
+              description = lib.mdDoc ''
+                Domain name to request a TLS certificate for.
+              '';
+            };
+
+            tls_letsencrypt_challenge_type = mkOption {
+              type = types.enum ["TLS-ALPN-01" "HTTP-01"];
+              default = "HTTP-01";
+              description = lib.mdDoc ''
+                Type of ACME challenge to use, currently supported types:
+                `HTTP-01` or `TLS-ALPN-01`.
+              '';
+            };
+
+            tls_letsencrypt_listen = mkOption {
+              type = types.nullOr types.str;
+              default = ":http";
+              description = lib.mdDoc ''
+                When HTTP-01 challenge is chosen, letsencrypt must set up a
+                verification endpoint, and it will be listening on:
+                `:http = port 80`.
+              '';
+            };
+
+            tls_cert_path = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = lib.mdDoc ''
+                Path to already created certificate.
+              '';
+            };
+
+            tls_key_path = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = lib.mdDoc ''
+                Path to key for already created certificate.
+              '';
+            };
+
+            acl_policy_path = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = lib.mdDoc ''
+                Path to a file containg ACL policies.
+              '';
+            };
+          };
+        };
       };
-
-
     };
-
   };
-  config = mkIf cfg.enable {
 
+  imports = [
+    # TODO address + port = listen_addr
+    (mkRenamedOptionModule ["services" "headscale" "serverUrl"] ["services" "headscale" "settings" "server_url"])
+    (mkRenamedOptionModule ["services" "headscale" "privateKeyFile"] ["services" "headscale" "settings" "private_key_path"])
+    (mkRenamedOptionModule ["services" "headscale" "derp" "urls"] ["services" "headscale" "settings" "derp" "urls"])
+    (mkRenamedOptionModule ["services" "headscale" "derp" "paths"] ["services" "headscale" "settings" "derp" "paths"])
+    (mkRenamedOptionModule ["services" "headscale" "derp" "autoUpdate"] ["services" "headscale" "settings" "derp" "auto_update_enable"])
+    (mkRenamedOptionModule ["services" "headscale" "derp" "updateFrequency"] ["services" "headscale" "settings" "derp" "update_frequency"])
+    (mkRenamedOptionModule ["services" "headscale" "ephemeralNodeInactivityTimeout"] ["services" "headscale" "settings" "ephemeral_node_inactivity_timeout"])
+    (mkRenamedOptionModule ["services" "headscale" "database" "type"] ["services" "headscale" "settings" "db_type"])
+    (mkRenamedOptionModule ["services" "headscale" "database" "path"] ["services" "headscale" "settings" "db_path"])
+    (mkRenamedOptionModule ["services" "headscale" "database" "host"] ["services" "headscale" "settings" "db_host"])
+    (mkRenamedOptionModule ["services" "headscale" "database" "port"] ["services" "headscale" "settings" "db_port"])
+    (mkRenamedOptionModule ["services" "headscale" "database" "name"] ["services" "headscale" "settings" "db_name"])
+    (mkRenamedOptionModule ["services" "headscale" "database" "user"] ["services" "headscale" "settings" "db_user"])
+    (mkRenamedOptionModule ["services" "headscale" "database" "passwordFile"] ["services" "headscale" "settings" "db_password_file"])
+    (mkRenamedOptionModule ["services" "headscale" "logLevel"] ["services" "headscale" "settings" "log" "level"])
+    (mkRenamedOptionModule ["services" "headscale" "dns" "nameservers"] ["services" "headscale" "settings" "dns_config" "nameservers"])
+    (mkRenamedOptionModule ["services" "headscale" "dns" "domains"] ["services" "headscale" "settings" "dns_config" "domains"])
+    (mkRenamedOptionModule ["services" "headscale" "dns" "magicDns"] ["services" "headscale" "settings" "dns_config" "magic_dns"])
+    (mkRenamedOptionModule ["services" "headscale" "dns" "baseDomain"] ["services" "headscale" "settings" "dns_config" "base_domain"])
+    (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "issuer"] ["services" "headscale" "settings" "oidc" "issuer"])
+    (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientId"] ["services" "headscale" "settings" "oidc" "client_id"])
+    (mkRenamedOptionModule ["services" "headscale" "openIdConnect" "clientSecretFile"] ["services" "headscale" "settings" "oidc" "client_secret_path"])
+    (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "hostname"] ["services" "headscale" "settings" "tls_letsencrypt_hostname"])
+    (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "challengeType"] ["services" "headscale" "settings" "tls_letsencrypt_challenge_type"])
+    (mkRenamedOptionModule ["services" "headscale" "tls" "letsencrypt" "httpListen"] ["services" "headscale" "settings" "tls_letsencrypt_listen"])
+    (mkRenamedOptionModule ["services" "headscale" "tls" "certFile"] ["services" "headscale" "settings" "tls_cert_path"])
+    (mkRenamedOptionModule ["services" "headscale" "tls" "keyFile"] ["services" "headscale" "settings" "tls_key_path"])
+    (mkRenamedOptionModule ["services" "headscale" "aclPolicyFile"] ["services" "headscale" "settings" "acl_policy_path"])
+
+    (mkRemovedOptionModule ["services" "headscale" "openIdConnect" "domainMap"] ''
+      Headscale no longer uses domain_map. If you're using an old version of headscale you can still set this option via services.headscale.settings.oidc.domain_map.
+    '')
+  ];
+
+  config = mkIf cfg.enable {
     services.headscale.settings = {
-      server_url = mkDefault cfg.serverUrl;
       listen_addr = mkDefault "${cfg.address}:${toString cfg.port}";
 
-      private_key_path = mkDefault cfg.privateKeyFile;
-
-      derp = {
-        urls = mkDefault cfg.derp.urls;
-        paths = mkDefault cfg.derp.paths;
-        auto_update_enable = mkDefault cfg.derp.autoUpdate;
-        update_frequency = mkDefault cfg.derp.updateFrequency;
-      };
-
       # Turn off update checks since the origin of our package
       # is nixpkgs and not Github.
       disable_check_updates = true;
 
-      ephemeral_node_inactivity_timeout = mkDefault cfg.ephemeralNodeInactivityTimeout;
-
-      db_type = mkDefault cfg.database.type;
-      db_path = mkDefault cfg.database.path;
-
-      log_level = mkDefault cfg.logLevel;
-
-      dns_config = {
-        nameservers = mkDefault cfg.dns.nameservers;
-        domains = mkDefault cfg.dns.domains;
-        magic_dns = mkDefault cfg.dns.magicDns;
-        base_domain = mkDefault cfg.dns.baseDomain;
-      };
-
       unix_socket = "${runDir}/headscale.sock";
 
-      # OpenID Connect
-      oidc = {
-        issuer = mkDefault cfg.openIdConnect.issuer;
-        client_id = mkDefault cfg.openIdConnect.clientId;
-        domain_map = mkDefault cfg.openIdConnect.domainMap;
-      };
-
       tls_letsencrypt_cache_dir = "${dataDir}/.cache";
-
-    } // optionalAttrs (cfg.database.host != null) {
-      db_host = mkDefault cfg.database.host;
-    } // optionalAttrs (cfg.database.port != null) {
-      db_port = mkDefault cfg.database.port;
-    } // optionalAttrs (cfg.database.name != null) {
-      db_name = mkDefault cfg.database.name;
-    } // optionalAttrs (cfg.database.user != null) {
-      db_user = mkDefault cfg.database.user;
-    } // optionalAttrs (cfg.tls.letsencrypt.hostname != null) {
-      tls_letsencrypt_hostname = mkDefault cfg.tls.letsencrypt.hostname;
-    } // optionalAttrs (cfg.tls.letsencrypt.challengeType != null) {
-      tls_letsencrypt_challenge_type = mkDefault cfg.tls.letsencrypt.challengeType;
-    } // optionalAttrs (cfg.tls.letsencrypt.httpListen != null) {
-      tls_letsencrypt_listen = mkDefault cfg.tls.letsencrypt.httpListen;
-    } // optionalAttrs (cfg.tls.certFile != null) {
-      tls_cert_path = mkDefault cfg.tls.certFile;
-    } // optionalAttrs (cfg.tls.keyFile != null) {
-      tls_key_path = mkDefault cfg.tls.keyFile;
-    } // optionalAttrs (cfg.aclPolicyFile != null) {
-      acl_policy_path = mkDefault cfg.aclPolicyFile;
     };
 
     # Setup the headscale configuration in a known path in /etc to
@@ -416,7 +456,7 @@ in
     # for communication.
     environment.etc."headscale/config.yaml".source = configFile;
 
-    users.groups.headscale = mkIf (cfg.group == "headscale") { };
+    users.groups.headscale = mkIf (cfg.group == "headscale") {};
 
     users.users.headscale = mkIf (cfg.user == "headscale") {
       description = "headscale user";
@@ -427,70 +467,65 @@ in
 
     systemd.services.headscale = {
       description = "headscale coordination server for Tailscale";
-      after = [ "network-online.target" ];
-      wantedBy = [ "multi-user.target" ];
-      restartTriggers = [ configFile ];
+      after = ["network-online.target"];
+      wantedBy = ["multi-user.target"];
+      restartTriggers = [configFile];
 
       environment.GIN_MODE = "release";
 
       script = ''
-        ${optionalString (cfg.database.passwordFile != null) ''
-          export HEADSCALE_DB_PASS="$(head -n1 ${escapeShellArg cfg.database.passwordFile})"
+        ${optionalString (cfg.settings.db_password_file != null) ''
+          export HEADSCALE_DB_PASS="$(head -n1 ${escapeShellArg cfg.settings.db_password_file})"
         ''}
 
-        ${optionalString (cfg.openIdConnect.clientSecretFile != null) ''
-          export HEADSCALE_OIDC_CLIENT_SECRET="$(head -n1 ${escapeShellArg cfg.openIdConnect.clientSecretFile})"
-        ''}
         exec ${cfg.package}/bin/headscale serve
       '';
 
-      serviceConfig =
-        let
-          capabilityBoundingSet = [ "CAP_CHOWN" ] ++ optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
-        in
-        {
-          Restart = "always";
-          Type = "simple";
-          User = cfg.user;
-          Group = cfg.group;
-
-          # Hardening options
-          RuntimeDirectory = "headscale";
-          # Allow headscale group access so users can be added and use the CLI.
-          RuntimeDirectoryMode = "0750";
-
-          StateDirectory = "headscale";
-          StateDirectoryMode = "0750";
-
-          ProtectSystem = "strict";
-          ProtectHome = true;
-          PrivateTmp = true;
-          PrivateDevices = true;
-          ProtectKernelTunables = true;
-          ProtectControlGroups = true;
-          RestrictSUIDSGID = true;
-          PrivateMounts = true;
-          ProtectKernelModules = true;
-          ProtectKernelLogs = true;
-          ProtectHostname = true;
-          ProtectClock = true;
-          ProtectProc = "invisible";
-          ProcSubset = "pid";
-          RestrictNamespaces = true;
-          RemoveIPC = true;
-          UMask = "0077";
-
-          CapabilityBoundingSet = capabilityBoundingSet;
-          AmbientCapabilities = capabilityBoundingSet;
-          NoNewPrivileges = true;
-          LockPersonality = true;
-          RestrictRealtime = true;
-          SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ];
-          SystemCallArchitectures = "native";
-          RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
-        };
+      serviceConfig = let
+        capabilityBoundingSet = ["CAP_CHOWN"] ++ optional (cfg.port < 1024) "CAP_NET_BIND_SERVICE";
+      in {
+        Restart = "always";
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+
+        # Hardening options
+        RuntimeDirectory = "headscale";
+        # Allow headscale group access so users can be added and use the CLI.
+        RuntimeDirectoryMode = "0750";
+
+        StateDirectory = "headscale";
+        StateDirectoryMode = "0750";
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        RestrictSUIDSGID = true;
+        PrivateMounts = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectProc = "invisible";
+        ProcSubset = "pid";
+        RestrictNamespaces = true;
+        RemoveIPC = true;
+        UMask = "0077";
+
+        CapabilityBoundingSet = capabilityBoundingSet;
+        AmbientCapabilities = capabilityBoundingSet;
+        NoNewPrivileges = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        SystemCallFilter = ["@system-service" "~@privileged" "@chown"];
+        SystemCallArchitectures = "native";
+        RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
+      };
     };
   };
 
-  meta.maintainers = with maintainers; [ kradalby ];
+  meta.maintainers = with maintainers; [kradalby misterio77];
 }
diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix
index 63bb44256dd..ecc158f8151 100644
--- a/nixos/modules/services/networking/hostapd.nix
+++ b/nixos/modules/services/networking/hostapd.nix
@@ -20,6 +20,8 @@ let
     ssid=${cfg.ssid}
     hw_mode=${cfg.hwMode}
     channel=${toString cfg.channel}
+    ieee80211n=1
+    ieee80211ac=1
     ${optionalString (cfg.countryCode != null) "country_code=${cfg.countryCode}"}
     ${optionalString (cfg.countryCode != null) "ieee80211d=1"}
 
@@ -34,6 +36,7 @@ let
 
     ${optionalString cfg.wpa ''
       wpa=2
+      wpa_pairwise=CCMP
       wpa_passphrase=${cfg.wpaPassphrase}
     ''}
     ${optionalString cfg.noScan "noscan=1"}
@@ -66,7 +69,6 @@ in
       };
 
       interface = mkOption {
-        default = "";
         example = "wlp2s0";
         type = types.str;
         description = lib.mdDoc ''
@@ -94,7 +96,8 @@ in
       };
 
       ssid = mkOption {
-        default = "nixos";
+        default = config.system.nixos.distroId;
+        defaultText = literalExpression "config.system.nixos.distroId";
         example = "mySpecialSSID";
         type = types.str;
         description = lib.mdDoc "SSID to be used in IEEE 802.11 management frames.";
diff --git a/nixos/modules/services/networking/imaginary.nix b/nixos/modules/services/networking/imaginary.nix
new file mode 100644
index 00000000000..a655903d103
--- /dev/null
+++ b/nixos/modules/services/networking/imaginary.nix
@@ -0,0 +1,113 @@
+{ lib, config, pkgs, utils, ... }:
+
+let
+  inherit (lib) mdDoc mkEnableOption mkIf mkOption types;
+
+  cfg = config.services.imaginary;
+in {
+  options.services.imaginary = {
+    enable = mkEnableOption (mdDoc "imaginary image processing microservice");
+
+    address = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = mdDoc ''
+        Bind address. Corresponds to the `-a` flag.
+        Set to `""` to bind to all addresses.
+      '';
+      example = "[::1]";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 8088;
+      description = mdDoc "Bind port. Corresponds to the `-p` flag.";
+    };
+
+    settings = mkOption {
+      description = mdDoc ''
+        Command line arguments passed to the imaginary executable, stripped of
+        the prefix `-`. See upstream's
+        [README](https://github.com/h2non/imaginary#command-line-usage) for all
+        options.
+      '';
+      type = types.submodule {
+        freeformType = with types; attrsOf (oneOf [
+          bool
+          int
+          (nonEmptyListOf str)
+          str
+        ]);
+
+        options = {
+          return-size = mkOption {
+            type = types.bool;
+            default = false;
+            description = mdDoc "Return the image size in the HTTP headers.";
+          };
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [ {
+      assertion = ! lib.hasAttr "a" cfg.settings;
+      message = "Use services.imaginary.address to specify the -a flag.";
+    } {
+      assertion = ! lib.hasAttr "p" cfg.settings;
+      message = "Use services.imaginary.port to specify the -p flag.";
+    } ];
+
+    systemd.services.imaginary = {
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = rec {
+        ExecStart = let
+          args = lib.mapAttrsToList (key: val:
+            "-" + key + "=" + lib.concatStringsSep "," (map toString (lib.toList val))
+          ) (cfg.settings // { a = cfg.address; p = cfg.port; });
+        in "${pkgs.imaginary}/bin/imaginary ${utils.escapeSystemdExecArgs args}";
+        ProtectProc = "invisible";
+        BindReadOnlyPaths = lib.optional (cfg.settings ? mount) cfg.settings.mount;
+        CapabilityBoundingSet = if cfg.port < 1024 then
+          [ "CAP_NET_BIND_SERVICE" ]
+        else
+          [ "" ];
+        AmbientCapabilities = CapabilityBoundingSet;
+        NoNewPrivileges = true;
+        DynamicUser = true;
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        TemporaryFileSystem = [ "/:ro" ];
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateUsers = cfg.port >= 1024;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = [
+          "AF_INET"
+          "AF_INET6"
+        ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        PrivateMounts = true;
+        SystemCallFilter = [
+          "@system-service"
+          "~@privileged"
+        ];
+        DevicePolicy = "closed";
+      };
+    };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ dotlambda ];
+  };
+}
diff --git a/nixos/modules/services/networking/ircd-hybrid/builder.sh b/nixos/modules/services/networking/ircd-hybrid/builder.sh
index 38312210df2..d9d2e4264df 100644
--- a/nixos/modules/services/networking/ircd-hybrid/builder.sh
+++ b/nixos/modules/services/networking/ircd-hybrid/builder.sh
@@ -1,3 +1,4 @@
+if [ -e .attrs.sh ]; then source .attrs.sh; fi
 source $stdenv/setup
 
 doSub() {
diff --git a/nixos/modules/services/networking/iscsi/root-initiator.nix b/nixos/modules/services/networking/iscsi/root-initiator.nix
index 4434fedce1e..895467cc674 100644
--- a/nixos/modules/services/networking/iscsi/root-initiator.nix
+++ b/nixos/modules/services/networking/iscsi/root-initiator.nix
@@ -185,6 +185,10 @@ in
         assertion = cfg.loginAll -> cfg.target == null;
         message = "iSCSI target name is set while login on all portals is enabled.";
       }
+      {
+        assertion = !config.boot.initrd.systemd.enable;
+        message = "systemd stage 1 does not support iscsi yet.";
+      }
     ];
   };
 }
diff --git a/nixos/modules/services/networking/ivpn.nix b/nixos/modules/services/networking/ivpn.nix
new file mode 100644
index 00000000000..6df630c1f19
--- /dev/null
+++ b/nixos/modules/services/networking/ivpn.nix
@@ -0,0 +1,51 @@
+{ config, lib, pkgs, ... }:
+let
+  cfg = config.services.ivpn;
+in
+with lib;
+{
+  options.services.ivpn = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        This option enables iVPN daemon.
+        This sets {option}`networking.firewall.checkReversePath` to "loose", which might be undesirable for security.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    boot.kernelModules = [ "tun" ];
+
+    environment.systemPackages = with pkgs; [ ivpn ivpn-service ];
+
+    # iVPN writes to /etc/iproute2/rt_tables
+    networking.iproute2.enable = true;
+    networking.firewall.checkReversePath = "loose";
+
+    systemd.services.ivpn-service = {
+      description = "iVPN daemon";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network.target" ];
+      after = [
+        "network-online.target"
+        "NetworkManager.service"
+        "systemd-resolved.service"
+      ];
+      path = [
+        # Needed for mount
+        "/run/wrappers"
+      ];
+      startLimitBurst = 5;
+      startLimitIntervalSec = 20;
+      serviceConfig = {
+        ExecStart = "${pkgs.ivpn-service}/bin/ivpn-service --logging";
+        Restart = "always";
+        RestartSec = 1;
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ ataraxiasjel ];
+}
diff --git a/nixos/modules/services/networking/jicofo.nix b/nixos/modules/services/networking/jicofo.nix
index 5e978896073..0886bbe004c 100644
--- a/nixos/modules/services/networking/jicofo.nix
+++ b/nixos/modules/services/networking/jicofo.nix
@@ -4,6 +4,15 @@ with lib;
 
 let
   cfg = config.services.jicofo;
+
+  # HOCON is a JSON superset that some jitsi-meet components use for configuration
+  toHOCON = x: if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}")
+    else if isAttrs x && x ? __hocon_unquoted_string then x.__hocon_unquoted_string
+    else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}"
+    else if isList x then "[${ concatMapStringsSep "," toHOCON x }]"
+    else builtins.toJSON x;
+
+  configFile = pkgs.writeText "jicofo.conf" (toHOCON cfg.config);
 in
 {
   options.services.jicofo = with types; {
@@ -68,22 +77,34 @@ in
     };
 
     config = mkOption {
-      type = attrsOf str;
+      type = (pkgs.formats.json {}).type;
       default = { };
       example = literalExpression ''
         {
-          "org.jitsi.jicofo.auth.URL" = "XMPP:jitsi-meet.example.com";
+          jicofo.bridge.max-bridge-participants = 42;
         }
       '';
       description = lib.mdDoc ''
-        Contents of the {file}`sip-communicator.properties` configuration file for jicofo.
+        Contents of the {file}`jicofo.conf` configuration file.
       '';
     };
   };
 
   config = mkIf cfg.enable {
-    services.jicofo.config = mapAttrs (_: v: mkDefault v) {
-      "org.jitsi.jicofo.BRIDGE_MUC" = cfg.bridgeMuc;
+    services.jicofo.config = {
+      jicofo = {
+        bridge.brewery-jid = cfg.bridgeMuc;
+        xmpp = rec {
+          client = {
+            hostname = cfg.xmppHost;
+            username = cfg.userName;
+            domain = cfg.userDomain;
+            password = { __hocon_envvar = "JICOFO_AUTH_PASS"; };
+            xmpp-domain = if cfg.xmppDomain == null then cfg.xmppHost else cfg.xmppDomain;
+          };
+          service = client;
+        };
+      };
     };
 
     users.groups.jitsi-meet = {};
@@ -93,6 +114,7 @@ in
         "-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION" = "/etc/jitsi";
         "-Dnet.java.sip.communicator.SC_HOME_DIR_NAME" = "jicofo";
         "-Djava.util.logging.config.file" = "/etc/jitsi/jicofo/logging.properties";
+        "-Dconfig.file" = configFile;
       };
     in
     {
@@ -101,18 +123,13 @@ in
       after = [ "network.target" ];
 
       restartTriggers = [
-        config.environment.etc."jitsi/jicofo/sip-communicator.properties".source
+        configFile
       ];
       environment.JAVA_SYS_PROPS = concatStringsSep " " (mapAttrsToList (k: v: "${k}=${toString v}") jicofoProps);
 
       script = ''
-        ${pkgs.jicofo}/bin/jicofo \
-          --host=${cfg.xmppHost} \
-          --domain=${if cfg.xmppDomain == null then cfg.xmppHost else cfg.xmppDomain} \
-          --secret=$(cat ${cfg.componentPasswordFile}) \
-          --user_name=${cfg.userName} \
-          --user_domain=${cfg.userDomain} \
-          --user_password=$(cat ${cfg.userPasswordFile})
+        export JICOFO_AUTH_PASS="$(<${cfg.userPasswordFile})"
+        exec "${pkgs.jicofo}/bin/jicofo"
       '';
 
       serviceConfig = {
@@ -140,10 +157,7 @@ in
       };
     };
 
-    environment.etc."jitsi/jicofo/sip-communicator.properties".source =
-      pkgs.writeText "sip-communicator.properties" (
-        generators.toKeyValue {} cfg.config
-      );
+    environment.etc."jitsi/jicofo/sip-communicator.properties".text = "";
     environment.etc."jitsi/jicofo/logging.properties".source =
       mkDefault "${pkgs.jicofo}/etc/jitsi/jicofo/logging.properties-journal";
   };
diff --git a/nixos/modules/services/networking/kresd.nix b/nixos/modules/services/networking/kresd.nix
index 55af6abd5e0..3ad757133a6 100644
--- a/nixos/modules/services/networking/kresd.nix
+++ b/nixos/modules/services/networking/kresd.nix
@@ -79,7 +79,7 @@ in {
       example = [ "53" ];
       description = lib.mdDoc ''
         What addresses and ports the server should listen on.
-        For detailed syntax see ListenStream in man systemd.socket.
+        For detailed syntax see ListenStream in {manpage}`systemd.socket(5)`.
       '';
     };
     listenTLS = mkOption {
@@ -88,7 +88,7 @@ in {
       example = [ "198.51.100.1:853" "[2001:db8::1]:853" "853" ];
       description = lib.mdDoc ''
         Addresses and ports on which kresd should provide DNS over TLS (see RFC 7858).
-        For detailed syntax see ListenStream in man systemd.socket.
+        For detailed syntax see ListenStream in {manpage}`systemd.socket(5)`.
       '';
     };
     listenDoH = mkOption {
@@ -97,7 +97,7 @@ in {
       example = [ "198.51.100.1:443" "[2001:db8::1]:443" "443" ];
       description = lib.mdDoc ''
         Addresses and ports on which kresd should provide DNS over HTTPS/2 (see RFC 8484).
-        For detailed syntax see ListenStream in man systemd.socket.
+        For detailed syntax see ListenStream in {manpage}`systemd.socket(5)`.
       '';
     };
     instances = mkOption {
diff --git a/nixos/modules/services/networking/minidlna.nix b/nixos/modules/services/networking/minidlna.nix
index 549f1fe5de3..d0de6cd4fdc 100644
--- a/nixos/modules/services/networking/minidlna.nix
+++ b/nixos/modules/services/networking/minidlna.nix
@@ -16,7 +16,7 @@ in
     description = lib.mdDoc ''
       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
+      such as televisions and media players. If you use the firewall, consider
       adding the following: `services.minidlna.openFirewall = true;`
     '';
   };
@@ -54,10 +54,7 @@ in
         description = lib.mdDoc ''
           The interval between announces (in seconds).
           Instead of waiting for announces, you should set `openFirewall` option to use SSDP discovery.
-          Furthermore, this option has been set to 90000 in order to prevent disconnects with certain
-          clients and relies solely on the discovery.
-
-          Lower values (e.g. 30 seconds) should be used if you can't use the discovery.
+          Lower values (e.g. 30 seconds) should be used if your network blocks the discovery unicast.
           Some relevant information can be found here:
           https://sourceforge.net/p/minidlna/discussion/879957/thread/1389d197/
         '';
@@ -82,8 +79,8 @@ in
       };
       options.root_container = mkOption {
         type = types.str;
-        default = ".";
-        example = "B";
+        default = "B";
+        example = ".";
         description = lib.mdDoc "Use a different container as the root of the directory tree presented to clients.";
       };
       options.log_level = mkOption {
@@ -133,22 +130,19 @@ in
 
     users.groups.minidlna.gid = config.ids.gids.minidlna;
 
-    systemd.services.minidlna =
-      { description = "MiniDLNA Server";
-
-        wantedBy = [ "multi-user.target" ];
-        after = [ "network.target" ];
+    systemd.services.minidlna = {
+      description = "MiniDLNA Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
 
-        serviceConfig =
-          { User = "minidlna";
-            Group = "minidlna";
-            CacheDirectory = "minidlna";
-            RuntimeDirectory = "minidlna";
-            PIDFile = "/run/minidlna/pid";
-            ExecStart =
-              "${pkgs.minidlna}/sbin/minidlnad -S -P /run/minidlna/pid" +
-              " -f ${settingsFile}";
-          };
+      serviceConfig = {
+        User = "minidlna";
+        Group = "minidlna";
+        CacheDirectory = "minidlna";
+        RuntimeDirectory = "minidlna";
+        PIDFile = "/run/minidlna/pid";
+        ExecStart = "${pkgs.minidlna}/sbin/minidlnad -S -P /run/minidlna/pid -f ${settingsFile}";
       };
+    };
   };
 }
diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix
index 270450cb0c6..a4fd2fd7c89 100644
--- a/nixos/modules/services/networking/mosquitto.nix
+++ b/nixos/modules/services/networking/mosquitto.nix
@@ -671,8 +671,6 @@ in
 
   meta = {
     maintainers = with lib.maintainers; [ pennae ];
-    # Don't edit the docbook xml directly, edit the md and generate it:
-    # `pandoc mosquitto.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > mosquitto.xml`
-    doc = ./mosquitto.xml;
+    doc = ./mosquitto.md;
   };
 }
diff --git a/nixos/modules/services/networking/mosquitto.xml b/nixos/modules/services/networking/mosquitto.xml
deleted file mode 100644
index d16ab28c026..00000000000
--- a/nixos/modules/services/networking/mosquitto.xml
+++ /dev/null
@@ -1,147 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-mosquitto">
-  <title>Mosquitto</title>
-  <para>
-    Mosquitto is a MQTT broker often used for IoT or home automation
-    data transport.
-  </para>
-  <section xml:id="module-services-mosquitto-quickstart">
-    <title>Quickstart</title>
-    <para>
-      A minimal configuration for Mosquitto is
-    </para>
-    <programlisting language="bash">
-services.mosquitto = {
-  enable = true;
-  listeners = [ {
-    acl = [ &quot;pattern readwrite #&quot; ];
-    omitPasswordAuth = true;
-    settings.allow_anonymous = true;
-  } ];
-};
-</programlisting>
-    <para>
-      This will start a broker on port 1883, listening on all interfaces
-      of the machine, allowing read/write access to all topics to any
-      user without password requirements.
-    </para>
-    <para>
-      User authentication can be configured with the
-      <literal>users</literal> key of listeners. A config that gives
-      full read access to a user <literal>monitor</literal> and
-      restricted write access to a user <literal>service</literal> could
-      look like
-    </para>
-    <programlisting language="bash">
-services.mosquitto = {
-  enable = true;
-  listeners = [ {
-    users = {
-      monitor = {
-        acl = [ &quot;read #&quot; ];
-        password = &quot;monitor&quot;;
-      };
-      service = {
-        acl = [ &quot;write service/#&quot; ];
-        password = &quot;service&quot;;
-      };
-    };
-  } ];
-};
-</programlisting>
-    <para>
-      TLS authentication is configured by setting TLS-related options of
-      the listener:
-    </para>
-    <programlisting language="bash">
-services.mosquitto = {
-  enable = true;
-  listeners = [ {
-    port = 8883; # port change is not required, but helpful to avoid mistakes
-    # ...
-    settings = {
-      cafile = &quot;/path/to/mqtt.ca.pem&quot;;
-      certfile = &quot;/path/to/mqtt.pem&quot;;
-      keyfile = &quot;/path/to/mqtt.key&quot;;
-    };
-  } ];
-</programlisting>
-  </section>
-  <section xml:id="module-services-mosquitto-config">
-    <title>Configuration</title>
-    <para>
-      The Mosquitto configuration has four distinct types of settings:
-      the global settings of the daemon, listeners, plugins, and
-      bridges. Bridges and listeners are part of the global
-      configuration, plugins are part of listeners. Users of the broker
-      are configured as parts of listeners rather than globally,
-      allowing configurations in which a given user is only allowed to
-      log in to the broker using specific listeners (eg to configure an
-      admin user with full access to all topics, but restricted to
-      localhost).
-    </para>
-    <para>
-      Almost all options of Mosquitto are available for configuration at
-      their appropriate levels, some as NixOS options written in camel
-      case, the remainders under <literal>settings</literal> with their
-      exact names in the Mosquitto config file. The exceptions are
-      <literal>acl_file</literal> (which is always set according to the
-      <literal>acl</literal> attributes of a listener and its users) and
-      <literal>per_listener_settings</literal> (which is always set to
-      <literal>true</literal>).
-    </para>
-    <section xml:id="module-services-mosquitto-config-passwords">
-      <title>Password authentication</title>
-      <para>
-        Mosquitto can be run in two modes, with a password file or
-        without. Each listener has its own password file, and different
-        listeners may use different password files. Password file
-        generation can be disabled by setting
-        <literal>omitPasswordAuth = true</literal> for a listener; in
-        this case it is necessary to either set
-        <literal>settings.allow_anonymous = true</literal> to allow all
-        logins, or to configure other authentication methods like TLS
-        client certificates with
-        <literal>settings.use_identity_as_username = true</literal>.
-      </para>
-      <para>
-        The default is to generate a password file for each listener
-        from the users configured to that listener. Users with no
-        configured password will not be added to the password file and
-        thus will not be able to use the broker.
-      </para>
-    </section>
-    <section xml:id="module-services-mosquitto-config-acl">
-      <title>ACL format</title>
-      <para>
-        Every listener has a Mosquitto <literal>acl_file</literal>
-        attached to it. This ACL is configured via two attributes of the
-        config:
-      </para>
-      <itemizedlist spacing="compact">
-        <listitem>
-          <para>
-            the <literal>acl</literal> attribute of the listener
-            configures pattern ACL entries and topic ACL entries for
-            anonymous users. Each entry must be prefixed with
-            <literal>pattern</literal> or <literal>topic</literal> to
-            distinguish between these two cases.
-          </para>
-        </listitem>
-        <listitem>
-          <para>
-            the <literal>acl</literal> attribute of every user
-            configures in the listener configured the ACL for that given
-            user. Only topic ACLs are supported by Mosquitto in this
-            setting, so no prefix is required or allowed.
-          </para>
-        </listitem>
-      </itemizedlist>
-      <para>
-        The default ACL for a listener is empty, disallowing all
-        accesses from all clients. To configure a completely open ACL,
-        set <literal>acl = [ &quot;pattern readwrite #&quot; ]</literal>
-        in the listener.
-      </para>
-    </section>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/networking/multipath.nix b/nixos/modules/services/networking/multipath.nix
index 54ee2a01568..bd403e109c2 100644
--- a/nixos/modules/services/networking/multipath.nix
+++ b/nixos/modules/services/networking/multipath.nix
@@ -513,23 +513,22 @@ in {
         ${indentLines 2 devices}
         }
 
-        ${optionalString (!isNull defaults) ''
+        ${optionalString (defaults != null) ''
           defaults {
           ${indentLines 2 defaults}
-            multipath_dir ${cfg.package}/lib/multipath
           }
         ''}
-        ${optionalString (!isNull blacklist) ''
+        ${optionalString (blacklist != null) ''
           blacklist {
           ${indentLines 2 blacklist}
           }
         ''}
-        ${optionalString (!isNull blacklist_exceptions) ''
+        ${optionalString (blacklist_exceptions != null) ''
           blacklist_exceptions {
           ${indentLines 2 blacklist_exceptions}
           }
         ''}
-        ${optionalString (!isNull overrides) ''
+        ${optionalString (overrides != null) ''
           overrides {
           ${indentLines 2 overrides}
           }
diff --git a/nixos/modules/services/networking/murmur.nix b/nixos/modules/services/networking/murmur.nix
index 32498ca25ea..9ec4f57ca43 100644
--- a/nixos/modules/services/networking/murmur.nix
+++ b/nixos/modules/services/networking/murmur.nix
@@ -42,6 +42,8 @@ let
     ${if cfg.sslKey  == "" then "" else "sslKey="+cfg.sslKey}
     ${if cfg.sslCa   == "" then "" else "sslCA="+cfg.sslCa}
 
+    ${lib.optionalString (cfg.dbus != null) "dbus=${cfg.dbus}"}
+
     ${cfg.extraConfig}
   '';
 in
@@ -282,6 +284,12 @@ in
           `murmur` is running.
         '';
       };
+
+      dbus = mkOption {
+        type = types.enum [ null "session" "system" ];
+        default = null;
+        description = lib.mdDoc "Enable D-Bus remote control. Set to the bus you want Murmur to connect to.";
+      };
     };
   };
 
@@ -325,5 +333,27 @@ in
         Group = "murmur";
       };
     };
+
+    # currently not included in upstream package, addition requested at
+    # https://github.com/mumble-voip/mumble/issues/6078
+    services.dbus.packages = mkIf (cfg.dbus == "system") [(pkgs.writeTextFile {
+      name = "murmur-dbus-policy";
+      text = ''
+        <!DOCTYPE busconfig PUBLIC
+          "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+          "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+        <busconfig>
+          <policy user="murmur">
+            <allow own="net.sourceforge.mumble.murmur"/>
+          </policy>
+
+          <policy context="default">
+            <allow send_destination="net.sourceforge.mumble.murmur"/>
+            <allow receive_sender="net.sourceforge.mumble.murmur"/>
+          </policy>
+        </busconfig>
+      '';
+      destination = "/share/dbus-1/system.d/murmur.conf";
+    })];
   };
 }
diff --git a/nixos/modules/services/networking/nat-iptables.nix b/nixos/modules/services/networking/nat-iptables.nix
new file mode 100644
index 00000000000..d1bed401fee
--- /dev/null
+++ b/nixos/modules/services/networking/nat-iptables.nix
@@ -0,0 +1,191 @@
+# This module enables Network Address Translation (NAT).
+# XXX: todo: support multiple upstream links
+# see http://yesican.chsoft.biz/lartc/MultihomedLinuxNetworking.html
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.networking.nat;
+
+  mkDest = externalIP:
+    if externalIP == null
+    then "-j MASQUERADE"
+    else "-j SNAT --to-source ${externalIP}";
+  dest = mkDest cfg.externalIP;
+  destIPv6 = mkDest cfg.externalIPv6;
+
+  # Whether given IP (plus optional port) is an IPv6.
+  isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
+
+  helpers = import ./helpers.nix { inherit config lib; };
+
+  flushNat = ''
+    ${helpers}
+    ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
+    ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true
+    ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true
+    ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
+    ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true
+    ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true
+    ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true
+    ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true
+    ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true
+
+    ${cfg.extraStopCommands}
+  '';
+
+  mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: ''
+    # We can't match on incoming interface in POSTROUTING, so
+    # mark packets coming from the internal interfaces.
+    ${concatMapStrings (iface: ''
+      ${iptables} -w -t nat -A nixos-nat-pre \
+        -i '${iface}' -j MARK --set-mark 1
+    '') cfg.internalInterfaces}
+
+    # NAT the marked packets.
+    ${optionalString (cfg.internalInterfaces != []) ''
+      ${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \
+        ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
+    ''}
+
+    # NAT packets coming from the internal IPs.
+    ${concatMapStrings (range: ''
+      ${iptables} -w -t nat -A nixos-nat-post \
+        -s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
+    '') internalIPs}
+
+    # NAT from external ports to internal ports.
+    ${concatMapStrings (fwd: ''
+      ${iptables} -w -t nat -A nixos-nat-pre \
+        -i ${toString cfg.externalInterface} -p ${fwd.proto} \
+        --dport ${builtins.toString fwd.sourcePort} \
+        -j DNAT --to-destination ${fwd.destination}
+
+      ${concatMapStrings (loopbackip:
+        let
+          matchIP          = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
+          m                = builtins.match "${matchIP}:([0-9-]+)" fwd.destination;
+          destinationIP    = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
+          destinationPorts = if m == null then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1);
+        in ''
+          # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
+          ${iptables} -w -t nat -A nixos-nat-out \
+            -d ${loopbackip} -p ${fwd.proto} \
+            --dport ${builtins.toString fwd.sourcePort} \
+            -j DNAT --to-destination ${fwd.destination}
+
+          # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT
+          ${iptables} -w -t nat -A nixos-nat-pre \
+            -d ${loopbackip} -p ${fwd.proto} \
+            --dport ${builtins.toString fwd.sourcePort} \
+            -j DNAT --to-destination ${fwd.destination}
+
+          ${iptables} -w -t nat -A nixos-nat-post \
+            -d ${destinationIP} -p ${fwd.proto} \
+            --dport ${destinationPorts} \
+            -j SNAT --to-source ${loopbackip}
+        '') fwd.loopbackIPs}
+    '') forwardPorts}
+  '';
+
+  setupNat = ''
+    ${helpers}
+    # Create subchains where we store rules
+    ip46tables -w -t nat -N nixos-nat-pre
+    ip46tables -w -t nat -N nixos-nat-post
+    ip46tables -w -t nat -N nixos-nat-out
+
+    ${mkSetupNat {
+      iptables = "iptables";
+      inherit dest;
+      inherit (cfg) internalIPs;
+      forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
+    }}
+
+    ${optionalString cfg.enableIPv6 (mkSetupNat {
+      iptables = "ip6tables";
+      dest = destIPv6;
+      internalIPs = cfg.internalIPv6s;
+      forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
+    })}
+
+    ${optionalString (cfg.dmzHost != null) ''
+      iptables -w -t nat -A nixos-nat-pre \
+        -i ${toString cfg.externalInterface} -j DNAT \
+        --to-destination ${cfg.dmzHost}
+    ''}
+
+    ${cfg.extraCommands}
+
+    # Append our chains to the nat tables
+    ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre
+    ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post
+    ip46tables -w -t nat -A OUTPUT -j nixos-nat-out
+  '';
+
+in
+
+{
+
+  options = {
+
+    networking.nat.extraCommands = mkOption {
+      type = types.lines;
+      default = "";
+      example = "iptables -A INPUT -p icmp -j ACCEPT";
+      description = lib.mdDoc ''
+        Additional shell commands executed as part of the nat
+        initialisation script.
+
+        This option is incompatible with the nftables based nat module.
+      '';
+    };
+
+    networking.nat.extraStopCommands = mkOption {
+      type = types.lines;
+      default = "";
+      example = "iptables -D INPUT -p icmp -j ACCEPT || true";
+      description = lib.mdDoc ''
+        Additional shell commands executed as part of the nat
+        teardown script.
+
+        This option is incompatible with the nftables based nat module.
+      '';
+    };
+
+  };
+
+
+  config = mkIf (!config.networking.nftables.enable)
+    (mkMerge [
+      ({ networking.firewall.extraCommands = mkBefore flushNat; })
+      (mkIf config.networking.nat.enable {
+
+        networking.firewall = mkIf config.networking.firewall.enable {
+          extraCommands = setupNat;
+          extraStopCommands = flushNat;
+        };
+
+        systemd.services = mkIf (!config.networking.firewall.enable) {
+          nat = {
+            description = "Network Address Translation";
+            wantedBy = [ "network.target" ];
+            after = [ "network-pre.target" "systemd-modules-load.service" ];
+            path = [ config.networking.firewall.package ];
+            unitConfig.ConditionCapability = "CAP_NET_ADMIN";
+
+            serviceConfig = {
+              Type = "oneshot";
+              RemainAfterExit = true;
+            };
+
+            script = flushNat + setupNat;
+
+            postStop = flushNat;
+          };
+        };
+      })
+    ]);
+}
diff --git a/nixos/modules/services/networking/nat-nftables.nix b/nixos/modules/services/networking/nat-nftables.nix
new file mode 100644
index 00000000000..483910a1665
--- /dev/null
+++ b/nixos/modules/services/networking/nat-nftables.nix
@@ -0,0 +1,184 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.networking.nat;
+
+  mkDest = externalIP:
+    if externalIP == null
+    then "masquerade"
+    else "snat ${externalIP}";
+  dest = mkDest cfg.externalIP;
+  destIPv6 = mkDest cfg.externalIPv6;
+
+  toNftSet = list: concatStringsSep ", " list;
+  toNftRange = ports: replaceStrings [ ":" ] [ "-" ] (toString ports);
+
+  ifaceSet = toNftSet (map (x: ''"${x}"'') cfg.internalInterfaces);
+  ipSet = toNftSet cfg.internalIPs;
+  ipv6Set = toNftSet cfg.internalIPv6s;
+  oifExpr = optionalString (cfg.externalInterface != null) ''oifname "${cfg.externalInterface}"'';
+
+  # Whether given IP (plus optional port) is an IPv6.
+  isIPv6 = ip: length (lib.splitString ":" ip) > 2;
+
+  splitIPPorts = IPPorts:
+    let
+      matchIP = if isIPv6 IPPorts then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
+      m = builtins.match "${matchIP}:([0-9-]+)" IPPorts;
+    in
+    {
+      IP = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 0;
+      ports = if m == null then throw "bad ip:ports `${IPPorts}'" else elemAt m 1;
+    };
+
+  mkTable = { ipVer, dest, ipSet, forwardPorts, dmzHost }:
+    let
+      # nftables does not support both port and port range as values in a dnat map.
+      # e.g. "dnat th dport map { 80 : 10.0.0.1 . 80, 443 : 10.0.0.2 . 900-1000 }"
+      # So we split them.
+      fwdPorts = filter (x: length (splitString "-" x.destination) == 1) forwardPorts;
+      fwdPortsRange = filter (x: length (splitString "-" x.destination) > 1) forwardPorts;
+
+      # nftables maps for port forward
+      # l4proto . dport : addr . port
+      toFwdMap = forwardPorts: toNftSet (map
+        (fwd:
+          with (splitIPPorts fwd.destination);
+          "${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}"
+        )
+        forwardPorts);
+      fwdMap = toFwdMap fwdPorts;
+      fwdRangeMap = toFwdMap fwdPortsRange;
+
+      # nftables maps for port forward loopback dnat
+      # daddr . l4proto . dport : addr . port
+      toFwdLoopDnatMap = forwardPorts: toNftSet (concatMap
+        (fwd: map
+          (loopbackip:
+            with (splitIPPorts fwd.destination);
+            "${loopbackip} . ${fwd.proto} . ${toNftRange fwd.sourcePort} : ${IP} . ${ports}"
+          )
+          fwd.loopbackIPs)
+        forwardPorts);
+      fwdLoopDnatMap = toFwdLoopDnatMap fwdPorts;
+      fwdLoopDnatRangeMap = toFwdLoopDnatMap fwdPortsRange;
+
+      # nftables set for port forward loopback snat
+      # daddr . l4proto . dport
+      fwdLoopSnatSet = toNftSet (map
+        (fwd:
+          with (splitIPPorts fwd.destination);
+          "${IP} . ${fwd.proto} . ${ports}"
+        )
+        forwardPorts);
+    in
+    ''
+      chain pre {
+        type nat hook prerouting priority dstnat;
+
+        ${optionalString (fwdMap != "") ''
+          iifname "${cfg.externalInterface}" dnat meta l4proto . th dport map { ${fwdMap} } comment "port forward"
+        ''}
+        ${optionalString (fwdRangeMap != "") ''
+          iifname "${cfg.externalInterface}" dnat meta l4proto . th dport map { ${fwdRangeMap} } comment "port forward"
+        ''}
+
+        ${optionalString (fwdLoopDnatMap != "") ''
+          dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from other hosts behind NAT"
+        ''}
+        ${optionalString (fwdLoopDnatRangeMap != "") ''
+          dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatRangeMap} } comment "port forward loopback from other hosts behind NAT"
+        ''}
+
+        ${optionalString (dmzHost != null) ''
+          iifname "${cfg.externalInterface}" dnat ${dmzHost} comment "dmz"
+        ''}
+      }
+
+      chain post {
+        type nat hook postrouting priority srcnat;
+
+        ${optionalString (ifaceSet != "") ''
+          iifname { ${ifaceSet} } ${oifExpr} ${dest} comment "from internal interfaces"
+        ''}
+        ${optionalString (ipSet != "") ''
+          ${ipVer} saddr { ${ipSet} } ${oifExpr} ${dest} comment "from internal IPs"
+        ''}
+
+        ${optionalString (fwdLoopSnatSet != "") ''
+          iifname != "${cfg.externalInterface}" ${ipVer} daddr . meta l4proto . th dport { ${fwdLoopSnatSet} } masquerade comment "port forward loopback snat"
+        ''}
+      }
+
+      chain out {
+        type nat hook output priority mangle;
+
+        ${optionalString (fwdLoopDnatMap != "") ''
+          dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatMap} } comment "port forward loopback from the host itself"
+        ''}
+        ${optionalString (fwdLoopDnatRangeMap != "") ''
+          dnat ${ipVer} daddr . meta l4proto . th dport map { ${fwdLoopDnatRangeMap} } comment "port forward loopback from the host itself"
+        ''}
+      }
+    '';
+
+in
+
+{
+
+  config = mkIf (config.networking.nftables.enable && cfg.enable) {
+
+    assertions = [
+      {
+        assertion = cfg.extraCommands == "";
+        message = "extraCommands is incompatible with the nftables based nat module: ${cfg.extraCommands}";
+      }
+      {
+        assertion = cfg.extraStopCommands == "";
+        message = "extraStopCommands is incompatible with the nftables based nat module: ${cfg.extraStopCommands}";
+      }
+      {
+        assertion = config.networking.nftables.rulesetFile == null;
+        message = "networking.nftables.rulesetFile conflicts with the nat module";
+      }
+    ];
+
+    networking.nftables.ruleset = ''
+      table ip nixos-nat {
+        ${mkTable {
+          ipVer = "ip";
+          inherit dest ipSet;
+          forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
+          inherit (cfg) dmzHost;
+        }}
+      }
+
+      ${optionalString cfg.enableIPv6 ''
+        table ip6 nixos-nat {
+          ${mkTable {
+            ipVer = "ip6";
+            dest = destIPv6;
+            ipSet = ipv6Set;
+            forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
+            dmzHost = null;
+          }}
+        }
+      ''}
+    '';
+
+    networking.firewall.extraForwardRules = optionalString config.networking.firewall.filterForward ''
+      ${optionalString (ifaceSet != "") ''
+        iifname { ${ifaceSet} } ${oifExpr} accept comment "from internal interfaces"
+      ''}
+      ${optionalString (ipSet != "") ''
+        ip saddr { ${ipSet} } ${oifExpr} accept comment "from internal IPs"
+      ''}
+      ${optionalString (ipv6Set != "") ''
+        ip6 saddr { ${ipv6Set} } ${oifExpr} accept comment "from internal IPv6s"
+      ''}
+    '';
+
+  };
+}
diff --git a/nixos/modules/services/networking/nat.nix b/nixos/modules/services/networking/nat.nix
index 0b70ae47ccf..3afe6fe0a97 100644
--- a/nixos/modules/services/networking/nat.nix
+++ b/nixos/modules/services/networking/nat.nix
@@ -7,219 +7,95 @@
 with lib;
 
 let
-  cfg = config.networking.nat;
-
-  mkDest = externalIP: if externalIP == null
-                       then "-j MASQUERADE"
-                       else "-j SNAT --to-source ${externalIP}";
-  dest = mkDest cfg.externalIP;
-  destIPv6 = mkDest cfg.externalIPv6;
-
-  # Whether given IP (plus optional port) is an IPv6.
-  isIPv6 = ip: builtins.length (lib.splitString ":" ip) > 2;
-
-  helpers = import ./helpers.nix { inherit config lib; };
-
-  flushNat = ''
-    ${helpers}
-    ip46tables -w -t nat -D PREROUTING -j nixos-nat-pre 2>/dev/null|| true
-    ip46tables -w -t nat -F nixos-nat-pre 2>/dev/null || true
-    ip46tables -w -t nat -X nixos-nat-pre 2>/dev/null || true
-    ip46tables -w -t nat -D POSTROUTING -j nixos-nat-post 2>/dev/null || true
-    ip46tables -w -t nat -F nixos-nat-post 2>/dev/null || true
-    ip46tables -w -t nat -X nixos-nat-post 2>/dev/null || true
-    ip46tables -w -t nat -D OUTPUT -j nixos-nat-out 2>/dev/null || true
-    ip46tables -w -t nat -F nixos-nat-out 2>/dev/null || true
-    ip46tables -w -t nat -X nixos-nat-out 2>/dev/null || true
-
-    ${cfg.extraStopCommands}
-  '';
-
-  mkSetupNat = { iptables, dest, internalIPs, forwardPorts }: ''
-    # We can't match on incoming interface in POSTROUTING, so
-    # mark packets coming from the internal interfaces.
-    ${concatMapStrings (iface: ''
-      ${iptables} -w -t nat -A nixos-nat-pre \
-        -i '${iface}' -j MARK --set-mark 1
-    '') cfg.internalInterfaces}
-
-    # NAT the marked packets.
-    ${optionalString (cfg.internalInterfaces != []) ''
-      ${iptables} -w -t nat -A nixos-nat-post -m mark --mark 1 \
-        ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
-    ''}
-
-    # NAT packets coming from the internal IPs.
-    ${concatMapStrings (range: ''
-      ${iptables} -w -t nat -A nixos-nat-post \
-        -s '${range}' ${optionalString (cfg.externalInterface != null) "-o ${cfg.externalInterface}"} ${dest}
-    '') internalIPs}
-
-    # NAT from external ports to internal ports.
-    ${concatMapStrings (fwd: ''
-      ${iptables} -w -t nat -A nixos-nat-pre \
-        -i ${toString cfg.externalInterface} -p ${fwd.proto} \
-        --dport ${builtins.toString fwd.sourcePort} \
-        -j DNAT --to-destination ${fwd.destination}
 
-      ${concatMapStrings (loopbackip:
-        let
-          matchIP          = if isIPv6 fwd.destination then "[[]([0-9a-fA-F:]+)[]]" else "([0-9.]+)";
-          m                = builtins.match "${matchIP}:([0-9-]+)" fwd.destination;
-          destinationIP    = if m == null then throw "bad ip:ports `${fwd.destination}'" else elemAt m 0;
-          destinationPorts = if m == null then throw "bad ip:ports `${fwd.destination}'" else builtins.replaceStrings ["-"] [":"] (elemAt m 1);
-        in ''
-          # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from the host itself
-          ${iptables} -w -t nat -A nixos-nat-out \
-            -d ${loopbackip} -p ${fwd.proto} \
-            --dport ${builtins.toString fwd.sourcePort} \
-            -j DNAT --to-destination ${fwd.destination}
-
-          # Allow connections to ${loopbackip}:${toString fwd.sourcePort} from other hosts behind NAT
-          ${iptables} -w -t nat -A nixos-nat-pre \
-            -d ${loopbackip} -p ${fwd.proto} \
-            --dport ${builtins.toString fwd.sourcePort} \
-            -j DNAT --to-destination ${fwd.destination}
-
-          ${iptables} -w -t nat -A nixos-nat-post \
-            -d ${destinationIP} -p ${fwd.proto} \
-            --dport ${destinationPorts} \
-            -j SNAT --to-source ${loopbackip}
-        '') fwd.loopbackIPs}
-    '') forwardPorts}
-  '';
-
-  setupNat = ''
-    ${helpers}
-    # Create subchains where we store rules
-    ip46tables -w -t nat -N nixos-nat-pre
-    ip46tables -w -t nat -N nixos-nat-post
-    ip46tables -w -t nat -N nixos-nat-out
-
-    ${mkSetupNat {
-      iptables = "iptables";
-      inherit dest;
-      inherit (cfg) internalIPs;
-      forwardPorts = filter (x: !(isIPv6 x.destination)) cfg.forwardPorts;
-    }}
-
-    ${optionalString cfg.enableIPv6 (mkSetupNat {
-      iptables = "ip6tables";
-      dest = destIPv6;
-      internalIPs = cfg.internalIPv6s;
-      forwardPorts = filter (x: isIPv6 x.destination) cfg.forwardPorts;
-    })}
-
-    ${optionalString (cfg.dmzHost != null) ''
-      iptables -w -t nat -A nixos-nat-pre \
-        -i ${toString cfg.externalInterface} -j DNAT \
-        --to-destination ${cfg.dmzHost}
-    ''}
-
-    ${cfg.extraCommands}
-
-    # Append our chains to the nat tables
-    ip46tables -w -t nat -A PREROUTING -j nixos-nat-pre
-    ip46tables -w -t nat -A POSTROUTING -j nixos-nat-post
-    ip46tables -w -t nat -A OUTPUT -j nixos-nat-out
-  '';
+  cfg = config.networking.nat;
 
 in
 
 {
 
-  ###### interface
-
   options = {
 
     networking.nat.enable = mkOption {
       type = types.bool;
       default = false;
-      description =
-        lib.mdDoc ''
-          Whether to enable Network Address Translation (NAT).
-        '';
+      description = lib.mdDoc ''
+        Whether to enable Network Address Translation (NAT).
+      '';
     };
 
     networking.nat.enableIPv6 = mkOption {
       type = types.bool;
       default = false;
-      description =
-        lib.mdDoc ''
-          Whether to enable IPv6 NAT.
-        '';
+      description = lib.mdDoc ''
+        Whether to enable IPv6 NAT.
+      '';
     };
 
     networking.nat.internalInterfaces = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       example = [ "eth0" ];
-      description =
-        lib.mdDoc ''
-          The interfaces for which to perform NAT. Packets coming from
-          these interface and destined for the external interface will
-          be rewritten.
-        '';
+      description = lib.mdDoc ''
+        The interfaces for which to perform NAT. Packets coming from
+        these interface and destined for the external interface will
+        be rewritten.
+      '';
     };
 
     networking.nat.internalIPs = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       example = [ "192.168.1.0/24" ];
-      description =
-        lib.mdDoc ''
-          The IP address ranges for which to perform NAT.  Packets
-          coming from these addresses (on any interface) and destined
-          for the external interface will be rewritten.
-        '';
+      description = lib.mdDoc ''
+        The IP address ranges for which to perform NAT.  Packets
+        coming from these addresses (on any interface) and destined
+        for the external interface will be rewritten.
+      '';
     };
 
     networking.nat.internalIPv6s = mkOption {
       type = types.listOf types.str;
-      default = [];
+      default = [ ];
       example = [ "fc00::/64" ];
-      description =
-        lib.mdDoc ''
-          The IPv6 address ranges for which to perform NAT.  Packets
-          coming from these addresses (on any interface) and destined
-          for the external interface will be rewritten.
-        '';
+      description = lib.mdDoc ''
+        The IPv6 address ranges for which to perform NAT.  Packets
+        coming from these addresses (on any interface) and destined
+        for the external interface will be rewritten.
+      '';
     };
 
     networking.nat.externalInterface = mkOption {
       type = types.nullOr types.str;
       default = null;
       example = "eth1";
-      description =
-        lib.mdDoc ''
-          The name of the external network interface.
-        '';
+      description = lib.mdDoc ''
+        The name of the external network interface.
+      '';
     };
 
     networking.nat.externalIP = mkOption {
       type = types.nullOr types.str;
       default = null;
       example = "203.0.113.123";
-      description =
-        lib.mdDoc ''
-          The public IP address to which packets from the local
-          network are to be rewritten.  If this is left empty, the
-          IP address associated with the external interface will be
-          used.
-        '';
+      description = lib.mdDoc ''
+        The public IP address to which packets from the local
+        network are to be rewritten.  If this is left empty, the
+        IP address associated with the external interface will be
+        used.
+      '';
     };
 
     networking.nat.externalIPv6 = mkOption {
       type = types.nullOr types.str;
       default = null;
       example = "2001:dc0:2001:11::175";
-      description =
-        lib.mdDoc ''
-          The public IPv6 address to which packets from the local
-          network are to be rewritten.  If this is left empty, the
-          IP address associated with the external interface will be
-          used.
-        '';
+      description = lib.mdDoc ''
+        The public IPv6 address to which packets from the local
+        network are to be rewritten.  If this is left empty, the
+        IP address associated with the external interface will be
+        used.
+      '';
     };
 
     networking.nat.forwardPorts = mkOption {
@@ -246,122 +122,75 @@ in
 
           loopbackIPs = mkOption {
             type = types.listOf types.str;
-            default = [];
+            default = [ ];
             example = literalExpression ''[ "55.1.2.3" ]'';
-            description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort' from the host itself and from other hosts behind NAT";
+            description = lib.mdDoc "Public IPs for NAT reflection; for connections to `loopbackip:sourcePort` from the host itself and from other hosts behind NAT";
           };
         };
       });
-      default = [];
+      default = [ ];
       example = [
         { sourcePort = 8080; destination = "10.0.0.1:80"; proto = "tcp"; }
         { sourcePort = 8080; destination = "[fc00::2]:80"; proto = "tcp"; }
       ];
-      description =
-        lib.mdDoc ''
-          List of forwarded ports from the external interface to
-          internal destinations by using DNAT. Destination can be
-          IPv6 if IPv6 NAT is enabled.
-        '';
+      description = lib.mdDoc ''
+        List of forwarded ports from the external interface to
+        internal destinations by using DNAT. Destination can be
+        IPv6 if IPv6 NAT is enabled.
+      '';
     };
 
     networking.nat.dmzHost = mkOption {
       type = types.nullOr types.str;
       default = null;
       example = "10.0.0.1";
-      description =
-        lib.mdDoc ''
-          The local IP address to which all traffic that does not match any
-          forwarding rule is forwarded.
-        '';
-    };
-
-    networking.nat.extraCommands = mkOption {
-      type = types.lines;
-      default = "";
-      example = "iptables -A INPUT -p icmp -j ACCEPT";
-      description =
-        lib.mdDoc ''
-          Additional shell commands executed as part of the nat
-          initialisation script.
-        '';
-    };
-
-    networking.nat.extraStopCommands = mkOption {
-      type = types.lines;
-      default = "";
-      example = "iptables -D INPUT -p icmp -j ACCEPT || true";
-      description =
-        lib.mdDoc ''
-          Additional shell commands executed as part of the nat
-          teardown script.
-        '';
+      description = lib.mdDoc ''
+        The local IP address to which all traffic that does not match any
+        forwarding rule is forwarded.
+      '';
     };
 
   };
 
 
-  ###### implementation
-
-  config = mkMerge [
-    { networking.firewall.extraCommands = mkBefore flushNat; }
-    (mkIf config.networking.nat.enable {
-
-      assertions = [
-        { assertion = cfg.enableIPv6           -> config.networking.enableIPv6;
-          message = "networking.nat.enableIPv6 requires networking.enableIPv6";
-        }
-        { assertion = (cfg.dmzHost != null)    -> (cfg.externalInterface != null);
-          message = "networking.nat.dmzHost requires networking.nat.externalInterface";
-        }
-        { assertion = (cfg.forwardPorts != []) -> (cfg.externalInterface != null);
-          message = "networking.nat.forwardPorts requires networking.nat.externalInterface";
-        }
-      ];
-
-      # Use the same iptables package as in config.networking.firewall.
-      # When the firewall is enabled, this should be deduplicated without any
-      # error.
-      environment.systemPackages = [ config.networking.firewall.package ];
-
-      boot = {
-        kernelModules = [ "nf_nat_ftp" ];
-        kernel.sysctl = {
-          "net.ipv4.conf.all.forwarding" = mkOverride 99 true;
-          "net.ipv4.conf.default.forwarding" = mkOverride 99 true;
-        } // optionalAttrs cfg.enableIPv6 {
-          # Do not prevent IPv6 autoconfiguration.
-          # See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>.
-          "net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
-          "net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
-
-          # Forward IPv6 packets.
-          "net.ipv6.conf.all.forwarding" = mkOverride 99 true;
-          "net.ipv6.conf.default.forwarding" = mkOverride 99 true;
-        };
-      };
-
-      networking.firewall = mkIf config.networking.firewall.enable {
-        extraCommands = setupNat;
-        extraStopCommands = flushNat;
+  config = mkIf config.networking.nat.enable {
+
+    assertions = [
+      {
+        assertion = cfg.enableIPv6 -> config.networking.enableIPv6;
+        message = "networking.nat.enableIPv6 requires networking.enableIPv6";
+      }
+      {
+        assertion = (cfg.dmzHost != null) -> (cfg.externalInterface != null);
+        message = "networking.nat.dmzHost requires networking.nat.externalInterface";
+      }
+      {
+        assertion = (cfg.forwardPorts != [ ]) -> (cfg.externalInterface != null);
+        message = "networking.nat.forwardPorts requires networking.nat.externalInterface";
+      }
+    ];
+
+    # Use the same iptables package as in config.networking.firewall.
+    # When the firewall is enabled, this should be deduplicated without any
+    # error.
+    environment.systemPackages = [ config.networking.firewall.package ];
+
+    boot = {
+      kernelModules = [ "nf_nat_ftp" ];
+      kernel.sysctl = {
+        "net.ipv4.conf.all.forwarding" = mkOverride 99 true;
+        "net.ipv4.conf.default.forwarding" = mkOverride 99 true;
+      } // optionalAttrs cfg.enableIPv6 {
+        # Do not prevent IPv6 autoconfiguration.
+        # See <http://strugglers.net/~andy/blog/2011/09/04/linux-ipv6-router-advertisements-and-forwarding/>.
+        "net.ipv6.conf.all.accept_ra" = mkOverride 99 2;
+        "net.ipv6.conf.default.accept_ra" = mkOverride 99 2;
+
+        # Forward IPv6 packets.
+        "net.ipv6.conf.all.forwarding" = mkOverride 99 true;
+        "net.ipv6.conf.default.forwarding" = mkOverride 99 true;
       };
+    };
 
-      systemd.services = mkIf (!config.networking.firewall.enable) { nat = {
-        description = "Network Address Translation";
-        wantedBy = [ "network.target" ];
-        after = [ "network-pre.target" "systemd-modules-load.service" ];
-        path = [ config.networking.firewall.package ];
-        unitConfig.ConditionCapability = "CAP_NET_ADMIN";
-
-        serviceConfig = {
-          Type = "oneshot";
-          RemainAfterExit = true;
-        };
-
-        script = flushNat + setupNat;
-
-        postStop = flushNat;
-      }; };
-    })
-  ];
+  };
 }
diff --git a/nixos/modules/services/networking/ndppd.nix b/nixos/modules/services/networking/ndppd.nix
index 98c58d2d5db..d221c95ae62 100644
--- a/nixos/modules/services/networking/ndppd.nix
+++ b/nixos/modules/services/networking/ndppd.nix
@@ -17,7 +17,7 @@ let
       ttl ${toString proxy.ttl}
       ${render proxy.rules (ruleNetworkName: rule: ''
       rule ${prefer rule.network ruleNetworkName} {
-        ${rule.method}${if rule.method == "iface" then " ${rule.interface}" else ""}
+        ${rule.method}${optionalString (rule.method == "iface") " ${rule.interface}"}
       }'')}
     }'')}
   '');
diff --git a/nixos/modules/services/networking/nebula.nix b/nixos/modules/services/networking/nebula.nix
index 2bedafc5d9f..e1a8c6740f5 100644
--- a/nixos/modules/services/networking/nebula.nix
+++ b/nixos/modules/services/networking/nebula.nix
@@ -68,6 +68,12 @@ in
               description = lib.mdDoc "Whether this node is a lighthouse.";
             };
 
+            isRelay = mkOption {
+              type = types.bool;
+              default = false;
+              description = lib.mdDoc "Whether this node is a relay.";
+            };
+
             lighthouses = mkOption {
               type = types.listOf types.str;
               default = [];
@@ -78,6 +84,15 @@ in
               example = [ "192.168.100.1" ];
             };
 
+            relays = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              description = lib.mdDoc ''
+                List of IPs of relays that this node should allow traffic from.
+              '';
+              example = [ "192.168.100.1" ];
+            };
+
             listen.host = mkOption {
               type = types.str;
               default = "0.0.0.0";
@@ -157,6 +172,11 @@ in
             am_lighthouse = netCfg.isLighthouse;
             hosts = netCfg.lighthouses;
           };
+          relay = {
+            am_relay = netCfg.isRelay;
+            relays = netCfg.relays;
+            use_relays = true;
+          };
           listen = {
             host = netCfg.listen.host;
             port = netCfg.listen.port;
@@ -173,25 +193,41 @@ in
         configFile = format.generate "nebula-config-${netName}.yml" settings;
         in
         {
-          # Create systemd service for Nebula.
+          # Create the systemd service for Nebula.
           "nebula@${netName}" = {
             description = "Nebula VPN service for ${netName}";
             wants = [ "basic.target" ];
             after = [ "basic.target" "network.target" ];
             before = [ "sshd.service" ];
             wantedBy = [ "multi-user.target" ];
-            serviceConfig = mkMerge [
-              {
-                Type = "simple";
-                Restart = "always";
-                ExecStart = "${netCfg.package}/bin/nebula -config ${configFile}";
-              }
-              # The service needs to launch as root to access the tun device, if it's enabled.
-              (mkIf netCfg.tun.disable {
-                User = networkId;
-                Group = networkId;
-              })
-            ];
+            serviceConfig = {
+              Type = "simple";
+              Restart = "always";
+              ExecStart = "${netCfg.package}/bin/nebula -config ${configFile}";
+              UMask = "0027";
+              CapabilityBoundingSet = "CAP_NET_ADMIN";
+              AmbientCapabilities = "CAP_NET_ADMIN";
+              LockPersonality = true;
+              NoNewPrivileges = true;
+              PrivateDevices = false; # needs access to /dev/net/tun (below)
+              DeviceAllow = "/dev/net/tun rw";
+              DevicePolicy = "closed";
+              PrivateTmp = true;
+              PrivateUsers = false; # CapabilityBoundingSet needs to apply to the host namespace
+              ProtectClock = true;
+              ProtectControlGroups = true;
+              ProtectHome = true;
+              ProtectHostname = true;
+              ProtectKernelLogs = true;
+              ProtectKernelModules = true;
+              ProtectKernelTunables = true;
+              ProtectProc = "invisible";
+              ProtectSystem = "strict";
+              RestrictNamespaces = true;
+              RestrictSUIDSGID = true;
+              User = networkId;
+              Group = networkId;
+            };
             unitConfig.StartLimitIntervalSec = 0; # ensure Restart=always is always honoured (networks can go down for arbitrarily long)
           };
         }) enabledNetworks);
@@ -202,7 +238,7 @@ in
 
     # Create the service users and groups.
     users.users = mkMerge (mapAttrsToList (netName: netCfg:
-      mkIf netCfg.tun.disable {
+      {
         ${nameToId netName} = {
           group = nameToId netName;
           description = "Nebula service user for network ${netName}";
@@ -210,9 +246,8 @@ in
         };
       }) enabledNetworks);
 
-    users.groups = mkMerge (mapAttrsToList (netName: netCfg:
-      mkIf netCfg.tun.disable {
-        ${nameToId netName} = {};
-      }) enabledNetworks);
+    users.groups = mkMerge (mapAttrsToList (netName: netCfg: {
+      ${nameToId netName} = {};
+    }) enabledNetworks);
   };
 }
diff --git a/nixos/modules/services/networking/netbird.nix b/nixos/modules/services/networking/netbird.nix
index 5bd9e9ca616..647c0ce3e6d 100644
--- a/nixos/modules/services/networking/netbird.nix
+++ b/nixos/modules/services/networking/netbird.nix
@@ -41,9 +41,10 @@ in {
       documentation = [ "https://netbird.io/docs/" ];
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
+      path = with pkgs; [
+        openresolv
+      ];
       serviceConfig = {
-        AmbientCapabilities = [ "CAP_NET_ADMIN" ];
-        DynamicUser = true;
         Environment = [
           "NB_CONFIG=/var/lib/netbird/config.json"
           "NB_LOG_FILE=console"
diff --git a/nixos/modules/services/networking/networkd-dispatcher.nix b/nixos/modules/services/networking/networkd-dispatcher.nix
new file mode 100644
index 00000000000..c5319ca7b88
--- /dev/null
+++ b/nixos/modules/services/networking/networkd-dispatcher.nix
@@ -0,0 +1,98 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.networkd-dispatcher;
+
+in {
+
+  options = {
+    services.networkd-dispatcher = {
+
+      enable = mkEnableOption (mdDoc ''
+        Networkd-dispatcher service for systemd-networkd connection status
+        change. See [https://gitlab.com/craftyguy/networkd-dispatcher](upstream instructions)
+        for usage.
+      '');
+
+      rules = mkOption {
+        default = {};
+        example = lib.literalExpression ''
+          { "restart-tor" = {
+              onState = ["routable" "off"];
+              script = '''
+                #!''${pkgs.runtimeShell}
+                if [[ $IFACE == "wlan0" && $AdministrativeState == "configured" ]]; then
+                  echo "Restarting Tor ..."
+                  systemctl restart tor
+                fi
+                exit 0
+              ''';
+            };
+          };
+        '';
+        description = lib.mdDoc ''
+          Declarative configuration of networkd-dispatcher rules. See
+          [https://gitlab.com/craftyguy/networkd-dispatcher](upstream instructions)
+          for an introduction and example scripts.
+        '';
+        type = types.attrsOf (types.submodule {
+          options = {
+            onState = mkOption {
+              type = types.listOf (types.enum [
+                "routable" "dormant" "no-carrier" "off" "carrier" "degraded"
+                "configuring" "configured"
+              ]);
+              default = null;
+              description = lib.mdDoc ''
+                List of names of the systemd-networkd operational states which
+                should trigger the script. See <https://www.freedesktop.org/software/systemd/man/networkctl.html>
+                for a description of the specific state type.
+              '';
+            };
+            script = mkOption {
+              type = types.lines;
+              description = lib.mdDoc ''
+                Shell commands executed on specified operational states.
+              '';
+            };
+          };
+        });
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd = {
+      packages = [ pkgs.networkd-dispatcher ];
+      services.networkd-dispatcher = {
+        wantedBy = [ "multi-user.target" ];
+        # Override existing ExecStart definition
+        serviceConfig.ExecStart = let
+          scriptDir = pkgs.symlinkJoin {
+            name = "networkd-dispatcher-script-dir";
+            paths = lib.mapAttrsToList (name: cfg:
+              (map(state:
+                pkgs.writeTextFile {
+                  inherit name;
+                  text = cfg.script;
+                  destination = "/${state}.d/${name}";
+                  executable = true;
+                }
+              ) cfg.onState)
+            ) cfg.rules;
+          };
+        in [
+          ""
+          "${pkgs.networkd-dispatcher}/bin/networkd-dispatcher -v --script-dir ${scriptDir} $networkd_dispatcher_args"
+        ];
+      };
+    };
+
+  };
+}
+
diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix
index 8166a8e7110..faff1dca89b 100644
--- a/nixos/modules/services/networking/nftables.nix
+++ b/nixos/modules/services/networking/nftables.nix
@@ -12,11 +12,9 @@ in
       default = false;
       description =
         lib.mdDoc ''
-          Whether to enable nftables.  nftables is a Linux-based packet
-          filtering framework intended to replace frameworks like iptables.
-
-          This conflicts with the standard networking firewall, so make sure to
-          disable it before using nftables.
+          Whether to enable nftables and use nftables based firewall if enabled.
+          nftables is a Linux-based packet filtering framework intended to
+          replace frameworks like iptables.
 
           Note that if you have Docker enabled you will not be able to use
           nftables without intervention. Docker uses iptables internally to
@@ -30,6 +28,32 @@ in
           <https://wiki.nftables.org/wiki-nftables/index.php/Troubleshooting#Question_4._How_do_nftables_and_iptables_interact_when_used_on_the_same_system.3F>.
         '';
     };
+
+    networking.nftables.checkRuleset = mkOption {
+      type = types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        Run `nft check` on the ruleset to spot syntax errors during build.
+        Because this is executed in a sandbox, the check might fail if it requires
+        access to any environmental factors or paths outside the Nix store.
+        To circumvent this, the ruleset file can be edited using the preCheckRuleset
+        option to work in the sandbox environment.
+      '';
+    };
+
+    networking.nftables.preCheckRuleset = mkOption {
+      type = types.lines;
+      default = "";
+      example = lib.literalExpression ''
+        sed 's/skgid meadow/skgid nogroup/g' -i ruleset.conf
+      '';
+      description = lib.mdDoc ''
+        This script gets run before the ruleset is checked. It can be used to
+        create additional files needed for the ruleset check to work, or modify
+        the ruleset for cases the build environment cannot cover.
+      '';
+    };
+
     networking.nftables.ruleset = mkOption {
       type = types.lines;
       default = "";
@@ -79,19 +103,17 @@ in
         lib.mdDoc ''
           The ruleset to be used with nftables.  Should be in a format that
           can be loaded using "/bin/nft -f".  The ruleset is updated atomically.
+          This option conflicts with rulesetFile.
         '';
     };
     networking.nftables.rulesetFile = mkOption {
-      type = types.path;
-      default = pkgs.writeTextFile {
-        name = "nftables-rules";
-        text = cfg.ruleset;
-      };
-      defaultText = literalMD ''a file with the contents of {option}`networking.nftables.ruleset`'';
+      type = types.nullOr types.path;
+      default = null;
       description =
         lib.mdDoc ''
           The ruleset file to be used with nftables.  Should be in a format that
           can be loaded using "nft -f".  The ruleset is updated atomically.
+          This option conflicts with ruleset and nftables based firewall.
         '';
     };
   };
@@ -99,10 +121,6 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
-    assertions = [{
-      assertion = config.networking.firewall.enable == false;
-      message = "You can not use nftables and iptables at the same time. networking.firewall.enable must be set to false.";
-    }];
     boot.blacklistedKernelModules = [ "ip_tables" ];
     environment.systemPackages = [ pkgs.nftables ];
     networking.networkmanager.firewallBackend = mkDefault "nftables";
@@ -113,11 +131,24 @@ in
       wantedBy = [ "multi-user.target" ];
       reloadIfChanged = true;
       serviceConfig = let
-        rulesScript = pkgs.writeScript "nftables-rules" ''
-          #! ${pkgs.nftables}/bin/nft -f
-          flush ruleset
-          include "${cfg.rulesetFile}"
-        '';
+        rulesScript = pkgs.writeTextFile {
+          name =  "nftables-rules";
+          executable = true;
+          text = ''
+            #! ${pkgs.nftables}/bin/nft -f
+            flush ruleset
+            ${if cfg.rulesetFile != null then ''
+              include "${cfg.rulesetFile}"
+            '' else cfg.ruleset}
+          '';
+          checkPhase = lib.optionalString cfg.checkRuleset ''
+            cp $out ruleset.conf
+            ${cfg.preCheckRuleset}
+            export NIX_REDIRECTS=/etc/protocols=${pkgs.buildPackages.iana-etc}/etc/protocols:/etc/services=${pkgs.buildPackages.iana-etc}/etc/services
+            LD_PRELOAD="${pkgs.buildPackages.libredirect}/lib/libredirect.so ${pkgs.buildPackages.lklWithFirewall.lib}/lib/liblkl-hijack.so" \
+              ${pkgs.buildPackages.nftables}/bin/nft --check --file ruleset.conf
+          '';
+        };
       in {
         Type = "oneshot";
         RemainAfterExit = true;
diff --git a/nixos/modules/services/networking/nomad.nix b/nixos/modules/services/networking/nomad.nix
index 890ee0b7d8d..b1e51195247 100644
--- a/nixos/modules/services/networking/nomad.nix
+++ b/nixos/modules/services/networking/nomad.nix
@@ -67,10 +67,21 @@ in
           Additional plugins dir used to configure nomad.
         '';
         example = literalExpression ''
-          [ "<pluginDir>" pkgs.<plugins-name> ]
+          [ "<pluginDir>" pkgs.nomad-driver-nix pkgs.nomad-driver-podman  ]
         '';
       };
 
+      credentials = mkOption {
+        description = lib.mdDoc ''
+          Credentials envs used to configure nomad secrets.
+        '';
+        type = types.attrsOf types.str;
+        default = { };
+
+        example = {
+          logs_remote_write_password = "/run/keys/nomad_write_password";
+        };
+      };
 
       settings = mkOption {
         type = format.type;
@@ -139,9 +150,17 @@ in
         {
           DynamicUser = cfg.dropPrivileges;
           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-          ExecStart = "${cfg.package}/bin/nomad agent -config=/etc/nomad.json" +
+          ExecStart =
+            let
+              pluginsDir = pkgs.symlinkJoin
+                {
+                  name = "nomad-plugins";
+                  paths = cfg.extraSettingsPlugins;
+                };
+            in
+            "${cfg.package}/bin/nomad agent -config=/etc/nomad.json -plugin-dir=${pluginsDir}/bin" +
             concatMapStrings (path: " -config=${path}") cfg.extraSettingsPaths +
-            concatMapStrings (path: " -plugin-dir=${path}/bin") cfg.extraSettingsPlugins;
+            concatMapStrings (key: " -config=\${CREDENTIALS_DIRECTORY}/${key}") (lib.attrNames cfg.credentials);
           KillMode = "process";
           KillSignal = "SIGINT";
           LimitNOFILE = 65536;
@@ -150,6 +169,7 @@ in
           Restart = "on-failure";
           RestartSec = 2;
           TasksMax = "infinity";
+          LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials;
         }
         (mkIf cfg.enableDocker {
           SupplementaryGroups = "docker"; # space-separated string
diff --git a/nixos/modules/services/networking/ntopng.nix b/nixos/modules/services/networking/ntopng.nix
index e6344d7ff3b..bf7ec19f02a 100644
--- a/nixos/modules/services/networking/ntopng.nix
+++ b/nixos/modules/services/networking/ntopng.nix
@@ -86,7 +86,7 @@ in
 
       redis.createInstance = mkOption {
         type = types.nullOr types.str;
-        default = if versionAtLeast config.system.stateVersion "22.05" then "ntopng" else "";
+        default = optionalString (versionAtLeast config.system.stateVersion "22.05") "ntopng";
         description = lib.mdDoc ''
           Local Redis instance name. Set to `null` to disable
           local Redis instance. Defaults to `""` for
diff --git a/nixos/modules/services/networking/ntp/chrony.nix b/nixos/modules/services/networking/ntp/chrony.nix
index 7e3bb565d10..2d421abc8be 100644
--- a/nixos/modules/services/networking/ntp/chrony.nix
+++ b/nixos/modules/services/networking/ntp/chrony.nix
@@ -147,9 +147,9 @@ in
     systemd.services.systemd-timedated.environment = { SYSTEMD_TIMEDATED_NTP_SERVICES = "chronyd.service"; };
 
     systemd.tmpfiles.rules = [
-      "d ${stateDir} 0755 chrony chrony - -"
-      "f ${driftFile} 0640 chrony chrony -"
-      "f ${keyFile} 0640 chrony chrony -"
+      "d ${stateDir} 0750 chrony chrony - -"
+      "f ${driftFile} 0640 chrony chrony - -"
+      "f ${keyFile} 0640 chrony chrony - -"
     ];
 
     systemd.services.chronyd =
@@ -164,15 +164,47 @@ in
         path = [ chronyPkg ];
 
         unitConfig.ConditionCapability = "CAP_SYS_TIME";
-        serviceConfig =
-          { Type = "simple";
-            ExecStart = "${chronyPkg}/bin/chronyd ${builtins.toString chronyFlags}";
-
-            ProtectHome = "yes";
-            ProtectSystem = "full";
-            PrivateTmp = "yes";
-          };
-
+        serviceConfig = {
+          Type = "simple";
+          ExecStart = "${chronyPkg}/bin/chronyd ${builtins.toString chronyFlags}";
+
+          # Proc filesystem
+          ProcSubset = "pid";
+          ProtectProc = "invisible";
+          # Access write directories
+          ReadWritePaths = [ "${stateDir}" ];
+          UMask = "0027";
+          # Capabilities
+          CapabilityBoundingSet = [ "CAP_CHOWN" "CAP_DAC_OVERRIDE" "CAP_NET_BIND_SERVICE" "CAP_SETGID" "CAP_SETUID" "CAP_SYS_RESOURCE" "CAP_SYS_TIME" ];
+          # Device Access
+          DeviceAllow = [ "char-pps rw" "char-ptp rw" "char-rtc rw" ];
+          DevicePolicy = "closed";
+          # Security
+          NoNewPrivileges = true;
+          # Sandboxing
+          ProtectSystem = "full";
+          ProtectHome = true;
+          PrivateTmp = true;
+          PrivateDevices = false;
+          PrivateUsers = false;
+          ProtectHostname = true;
+          ProtectClock = false;
+          ProtectKernelTunables = true;
+          ProtectKernelModules = true;
+          ProtectKernelLogs = true;
+          ProtectControlGroups = true;
+          RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+          RestrictNamespaces = true;
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          RemoveIPC = true;
+          PrivateMounts = true;
+          # System Call Filtering
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "~@cpu-emulation @debug @keyring @mount @obsolete @privileged @resources" "@clock" "@setuid" "capset" "@chown" ];
+        };
       };
   };
 }
diff --git a/nixos/modules/services/networking/openconnect.nix b/nixos/modules/services/networking/openconnect.nix
index 469f0a3bc3b..7f9006053b8 100644
--- a/nixos/modules/services/networking/openconnect.nix
+++ b/nixos/modules/services/networking/openconnect.nix
@@ -32,6 +32,7 @@ let
         description = lib.mdDoc "Username to authenticate with.";
         example = "example-user";
         type = types.nullOr types.str;
+        default = null;
       };
 
       # Note: It does not make sense to provide a way to declaratively
@@ -89,6 +90,7 @@ let
   generateConfig = name: icfg:
     pkgs.writeText "config" ''
       interface=${name}
+      ${optionalString (icfg.protocol != null) "protocol=${icfg.protocol}"}
       ${optionalString (icfg.user != null) "user=${icfg.user}"}
       ${optionalString (icfg.passwordFile != null) "passwd-on-stdin"}
       ${optionalString (icfg.certificate != null)
@@ -108,14 +110,14 @@ let
       ExecStart = "${openconnect}/bin/openconnect --config=${
           generateConfig name icfg
         } ${icfg.gateway}";
-      StandardInput = "file:${icfg.passwordFile}";
+      StandardInput = lib.mkIf (icfg.passwordFile != null) "file:${icfg.passwordFile}";
 
       ProtectHome = true;
     };
   };
 in {
   options.networking.openconnect = {
-    package = mkPackageOption pkgs "openconnect" { };
+    package = mkPackageOptionMD pkgs "openconnect" { };
 
     interfaces = mkOption {
       description = lib.mdDoc "OpenConnect interfaces.";
diff --git a/nixos/modules/services/networking/openvpn.nix b/nixos/modules/services/networking/openvpn.nix
index 492a0936fdb..9a5866f2afd 100644
--- a/nixos/modules/services/networking/openvpn.nix
+++ b/nixos/modules/services/networking/openvpn.nix
@@ -14,7 +14,6 @@ let
       path = makeBinPath (getAttr "openvpn-${name}" config.systemd.services).path;
 
       upScript = ''
-        #! /bin/sh
         export PATH=${path}
 
         # For convenience in client scripts, extract the remote domain
@@ -34,7 +33,6 @@ let
       '';
 
       downScript = ''
-        #! /bin/sh
         export PATH=${path}
         ${optionalString cfg.updateResolvConf
            "${pkgs.update-resolv-conf}/libexec/openvpn/update-resolv-conf"}
@@ -47,9 +45,9 @@ let
           ${optionalString (cfg.up != "" || cfg.down != "" || cfg.updateResolvConf) "script-security 2"}
           ${cfg.config}
           ${optionalString (cfg.up != "" || cfg.updateResolvConf)
-              "up ${pkgs.writeScript "openvpn-${name}-up" upScript}"}
+              "up ${pkgs.writeShellScript "openvpn-${name}-up" upScript}"}
           ${optionalString (cfg.down != "" || cfg.updateResolvConf)
-              "down ${pkgs.writeScript "openvpn-${name}-down" downScript}"}
+              "down ${pkgs.writeShellScript "openvpn-${name}-down" downScript}"}
           ${optionalString (cfg.authUserPass != null)
               "auth-user-pass ${pkgs.writeText "openvpn-credentials-${name}" ''
                 ${cfg.authUserPass.username}
@@ -57,7 +55,8 @@ let
               ''}"}
         '';
 
-    in {
+    in
+    {
       description = "OpenVPN instance ‘${name}’";
 
       wantedBy = optional cfg.autoStart "multi-user.target";
@@ -70,6 +69,16 @@ let
       serviceConfig.Type = "notify";
     };
 
+  restartService = optionalAttrs cfg.restartAfterSleep {
+    openvpn-restart = {
+      wantedBy = [ "sleep.target" ];
+      path = [ pkgs.procps ];
+      script = "pkill --signal SIGHUP --exact openvpn";
+      #SIGHUP makes openvpn process to self-exit and then it got restarted by systemd because of Restart=always
+      description = "Sends a signal to OpenVPN process to trigger a restart after return from sleep";
+    };
+  };
+
 in
 
 {
@@ -82,7 +91,7 @@ in
   options = {
 
     services.openvpn.servers = mkOption {
-      default = {};
+      default = { };
 
       example = literalExpression ''
         {
@@ -201,14 +210,21 @@ in
 
     };
 
+    services.openvpn.restartAfterSleep = mkOption {
+      default = true;
+      type = types.bool;
+      description = lib.mdDoc "Whether OpenVPN client should be restarted after sleep.";
+    };
+
   };
 
 
   ###### implementation
 
-  config = mkIf (cfg.servers != {}) {
+  config = mkIf (cfg.servers != { }) {
 
-    systemd.services = listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) cfg.servers);
+    systemd.services = (listToAttrs (mapAttrsFlatten (name: value: nameValuePair "openvpn-${name}" (makeOpenVPNJob value name)) cfg.servers))
+      // restartService;
 
     environment.systemPackages = [ openvpn ];
 
diff --git a/nixos/modules/services/networking/peroxide.nix b/nixos/modules/services/networking/peroxide.nix
new file mode 100644
index 00000000000..885ee1d96cd
--- /dev/null
+++ b/nixos/modules/services/networking/peroxide.nix
@@ -0,0 +1,131 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.peroxide;
+  settingsFormat = pkgs.formats.yaml { };
+  stateDir = "peroxide";
+in
+{
+  options.services.peroxide = {
+    enable = mkEnableOption (lib.mdDoc "peroxide");
+
+    package = mkPackageOptionMD pkgs "peroxide" {
+      default = [ "peroxide" ];
+    };
+
+    logLevel = mkOption {
+      # https://github.com/sirupsen/logrus#level-logging
+      type = types.enum [ "Panic" "Fatal" "Error" "Warning" "Info" "Debug" "Trace" ];
+      default = "Warning";
+      example = "Info";
+      description = lib.mdDoc "Only log messages of this priority or higher.";
+    };
+
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+
+        options = {
+          UserPortImap = mkOption {
+            type = types.port;
+            default = 1143;
+            description = lib.mdDoc "The port on which to listen for IMAP connections.";
+          };
+
+          UserPortSmtp = mkOption {
+            type = types.port;
+            default = 1025;
+            description = lib.mdDoc "The port on which to listen for SMTP connections.";
+          };
+
+          ServerAddress = mkOption {
+            type = types.str;
+            default = "[::0]";
+            example = "localhost";
+            description = lib.mdDoc "The address on which to listen for connections.";
+          };
+        };
+      };
+      default = { };
+      description = lib.mdDoc ''
+        Configuration for peroxide.  See
+        [config.example.yaml](https://github.com/ljanyst/peroxide/blob/master/config.example.yaml)
+        for an example configuration.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.peroxide.settings = {
+      # peroxide deletes the cache directory on startup, which requires write
+      # permission on the parent directory, so we can't use
+      # /var/cache/peroxide
+      CacheDir = "/var/cache/peroxide/cache";
+      X509Key = mkDefault "/var/lib/${stateDir}/key.pem";
+      X509Cert = mkDefault "/var/lib/${stateDir}/cert.pem";
+      CookieJar = "/var/lib/${stateDir}/cookies.json";
+      CredentialsStore = "/var/lib/${stateDir}/credentials.json";
+    };
+
+    users.users.peroxide = {
+      isSystemUser = true;
+      group = "peroxide";
+    };
+    users.groups.peroxide = { };
+
+    systemd.services.peroxide = {
+      description = "Peroxide ProtonMail bridge";
+      requires = [ "network.target" ];
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      restartTriggers = [ config.environment.etc."peroxide.conf".source ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = "peroxide";
+        LogsDirectory = "peroxide";
+        LogsDirectoryMode = "0750";
+        # Specify just "peroxide" so that the user has write permission, because
+        # peroxide deletes and recreates the cache directory on startup.
+        CacheDirectory = [ "peroxide" "peroxide/cache" ];
+        CacheDirectoryMode = "0700";
+        StateDirectory = stateDir;
+        StateDirectoryMode = "0700";
+        ExecStart = "${cfg.package}/bin/peroxide -log-file=/var/log/peroxide/peroxide.log -log-level ${cfg.logLevel}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+
+      preStart = ''
+        # Create a self-signed certificate if no certificate exists.
+        if [[ ! -e "${cfg.settings.X509Key}" && ! -e "${cfg.settings.X509Cert}" ]]; then
+            ${cfg.package}/bin/peroxide-cfg -action gen-x509 \
+              -x509-org 'N/A' \
+              -x509-cn 'nixos' \
+              -x509-cert "${cfg.settings.X509Cert}" \
+              -x509-key "${cfg.settings.X509Key}"
+        fi
+      '';
+    };
+
+    # https://github.com/ljanyst/peroxide/blob/master/peroxide.logrotate
+    services.logrotate.settings.peroxide = {
+      files = "/var/log/peroxide/peroxide.log";
+      rotate = 31;
+      frequency = "daily";
+      compress = true;
+      delaycompress = true;
+      missingok = true;
+      notifempty = true;
+      su = "peroxide peroxide";
+      postrotate = "systemctl reload peroxide";
+    };
+
+    environment.etc."peroxide.conf".source = settingsFormat.generate "peroxide.conf" cfg.settings;
+    environment.systemPackages = [ cfg.package ];
+  };
+
+  meta.maintainers = with maintainers; [ aanderse aidalgol ];
+}
diff --git a/nixos/modules/services/networking/picosnitch.nix b/nixos/modules/services/networking/picosnitch.nix
new file mode 100644
index 00000000000..c9b38c1929c
--- /dev/null
+++ b/nixos/modules/services/networking/picosnitch.nix
@@ -0,0 +1,26 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.picosnitch;
+in
+{
+  options.services.picosnitch = {
+    enable = mkEnableOption (lib.mdDoc "picosnitch daemon");
+  };
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.picosnitch ];
+    systemd.services.picosnitch = {
+      description = "picosnitch";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        Restart = "always";
+        RestartSec = 5;
+        ExecStart = "${pkgs.picosnitch}/bin/picosnitch start-no-daemon";
+        PIDFile = "/run/picosnitch/picosnitch.pid";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/pixiecore.nix b/nixos/modules/services/networking/pixiecore.nix
index ea4008d4d51..f410be47164 100644
--- a/nixos/modules/services/networking/pixiecore.nix
+++ b/nixos/modules/services/networking/pixiecore.nix
@@ -23,7 +23,7 @@ in
       mode = mkOption {
         description = lib.mdDoc "Which mode to use";
         default = "boot";
-        type = types.enum [ "api" "boot" ];
+        type = types.enum [ "api" "boot" "quick" ];
       };
 
       debug = mkOption {
@@ -38,6 +38,12 @@ in
         description = lib.mdDoc "Handle DHCP traffic without binding to the DHCP server port";
       };
 
+      quick = mkOption {
+        description = lib.mdDoc "Which quick option to use";
+        default = "xyz";
+        type = types.enum [ "arch" "centos" "coreos" "debian" "fedora" "ubuntu" "xyz" ];
+      };
+
       kernel = mkOption {
         type = types.str or types.path;
         default = "";
@@ -117,6 +123,8 @@ in
               then [ "boot" cfg.kernel ]
                    ++ optional (cfg.initrd != "") cfg.initrd
                    ++ optionals (cfg.cmdLine != "") [ "--cmdline" cfg.cmdLine ]
+              else if cfg.mode == "quick"
+              then [ "quick" cfg.quick ]
               else [ "api" cfg.apiServer ];
           in
             ''
diff --git a/nixos/modules/services/networking/pleroma.md b/nixos/modules/services/networking/pleroma.md
new file mode 100644
index 00000000000..7c499e1c616
--- /dev/null
+++ b/nixos/modules/services/networking/pleroma.md
@@ -0,0 +1,180 @@
+# Pleroma {#module-services-pleroma}
+
+[Pleroma](https://pleroma.social/) is a lightweight activity pub server.
+
+## Generating the Pleroma config {#module-services-pleroma-generate-config}
+
+The `pleroma_ctl` CLI utility will prompt you some questions and it will generate an initial config file. This is an example of usage
+```ShellSession
+$ mkdir tmp-pleroma
+$ cd tmp-pleroma
+$ nix-shell -p pleroma-otp
+$ pleroma_ctl instance gen --output config.exs --output-psql setup.psql
+```
+
+The `config.exs` file can be further customized following the instructions on the [upstream documentation](https://docs-develop.pleroma.social/backend/configuration/cheatsheet/). Many refinements can be applied also after the service is running.
+
+## Initializing the database {#module-services-pleroma-initialize-db}
+
+First, the Postgresql service must be enabled in the NixOS configuration
+```
+services.postgresql = {
+  enable = true;
+  package = pkgs.postgresql_13;
+};
+```
+and activated with the usual
+```ShellSession
+$ nixos-rebuild switch
+```
+
+Then you can create and seed the database, using the `setup.psql` file that you generated in the previous section, by running
+```ShellSession
+$ sudo -u postgres psql -f setup.psql
+```
+
+## Enabling the Pleroma service locally {#module-services-pleroma-enable}
+
+In this section we will enable the Pleroma service only locally, so its configurations can be improved incrementally.
+
+This is an example of configuration, where [](#opt-services.pleroma.configs) option contains the content of the file `config.exs`, generated [in the first section](#module-services-pleroma-generate-config), but with the secrets (database password, endpoint secret key, salts, etc.) removed. Removing secrets is important, because otherwise they will be stored publicly in the Nix store.
+```
+services.pleroma = {
+  enable = true;
+  secretConfigFile = "/var/lib/pleroma/secrets.exs";
+  configs = [
+    ''
+    import Config
+
+    config :pleroma, Pleroma.Web.Endpoint,
+      url: [host: "pleroma.example.net", scheme: "https", port: 443],
+      http: [ip: {127, 0, 0, 1}, port: 4000]
+
+    config :pleroma, :instance,
+      name: "Test",
+      email: "admin@example.net",
+      notify_email: "admin@example.net",
+      limit: 5000,
+      registrations_open: true
+
+    config :pleroma, :media_proxy,
+      enabled: false,
+      redirect_on_failure: true
+
+    config :pleroma, Pleroma.Repo,
+      adapter: Ecto.Adapters.Postgres,
+      username: "pleroma",
+      database: "pleroma",
+      hostname: "localhost"
+
+    # Configure web push notifications
+    config :web_push_encryption, :vapid_details,
+      subject: "mailto:admin@example.net"
+
+    # ... TO CONTINUE ...
+    ''
+  ];
+};
+```
+
+Secrets must be moved into a file pointed by [](#opt-services.pleroma.secretConfigFile), in our case `/var/lib/pleroma/secrets.exs`. This file can be created copying the previously generated `config.exs` file and then removing all the settings, except the secrets. This is an example
+```
+# Pleroma instance passwords
+
+import Config
+
+config :pleroma, Pleroma.Web.Endpoint,
+   secret_key_base: "<the secret generated by pleroma_ctl>",
+   signing_salt: "<the secret generated by pleroma_ctl>"
+
+config :pleroma, Pleroma.Repo,
+  password: "<the secret generated by pleroma_ctl>"
+
+# Configure web push notifications
+config :web_push_encryption, :vapid_details,
+  public_key: "<the secret generated by pleroma_ctl>",
+  private_key: "<the secret generated by pleroma_ctl>"
+
+# ... TO CONTINUE ...
+```
+Note that the lines of the same configuration group are comma separated (i.e. all the lines end with a comma, except the last one), so when the lines with passwords are added or removed, commas must be adjusted accordingly.
+
+The service can be enabled with the usual
+```ShellSession
+$ nixos-rebuild switch
+```
+
+The service is accessible only from the local `127.0.0.1:4000` port. It can be tested using a port forwarding like this
+```ShellSession
+$ ssh -L 4000:localhost:4000 myuser@example.net
+```
+and then accessing <http://localhost:4000> from a web browser.
+
+## Creating the admin user {#module-services-pleroma-admin-user}
+
+After Pleroma service is running, all [Pleroma administration utilities](https://docs-develop.pleroma.social/) can be used. In particular an admin user can be created with
+```ShellSession
+$ pleroma_ctl user new <nickname> <email>  --admin --moderator --password <password>
+```
+
+## Configuring Nginx {#module-services-pleroma-nginx}
+
+In this configuration, Pleroma is listening only on the local port 4000. Nginx can be configured as a Reverse Proxy, for forwarding requests from public ports to the Pleroma service. This is an example of configuration, using
+[Let's Encrypt](https://letsencrypt.org/) for the TLS certificates
+```
+security.acme = {
+  email = "root@example.net";
+  acceptTerms = true;
+};
+
+services.nginx = {
+  enable = true;
+  addSSL = true;
+
+  recommendedTlsSettings = true;
+  recommendedOptimisation = true;
+  recommendedGzipSettings = true;
+
+  recommendedProxySettings = false;
+  # NOTE: if enabled, the NixOS proxy optimizations will override the Pleroma
+  # specific settings, and they will enter in conflict.
+
+  virtualHosts = {
+    "pleroma.example.net" = {
+      http2 = true;
+      enableACME = true;
+      forceSSL = true;
+
+      locations."/" = {
+        proxyPass = "http://127.0.0.1:4000";
+
+        extraConfig = ''
+          etag on;
+          gzip on;
+
+          add_header 'Access-Control-Allow-Origin' '*' always;
+          add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
+          add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
+          add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
+          if ($request_method = OPTIONS) {
+            return 204;
+          }
+          add_header X-XSS-Protection "1; mode=block";
+          add_header X-Permitted-Cross-Domain-Policies none;
+          add_header X-Frame-Options DENY;
+          add_header X-Content-Type-Options nosniff;
+          add_header Referrer-Policy same-origin;
+          add_header X-Download-Options noopen;
+          proxy_http_version 1.1;
+          proxy_set_header Upgrade $http_upgrade;
+          proxy_set_header Connection "upgrade";
+          proxy_set_header Host $host;
+
+          client_max_body_size 16m;
+          # NOTE: increase if users need to upload very big files
+        '';
+      };
+    };
+  };
+};
+```
diff --git a/nixos/modules/services/networking/pleroma.nix b/nixos/modules/services/networking/pleroma.nix
index 188598ea7b8..e9db7f3eab8 100644
--- a/nixos/modules/services/networking/pleroma.nix
+++ b/nixos/modules/services/networking/pleroma.nix
@@ -141,9 +141,11 @@ in {
         NoNewPrivileges = true;
         CapabilityBoundingSet = "~CAP_SYS_ADMIN";
       };
+      # disksup requires bash
+      path = [ pkgs.bash ];
     };
 
   };
   meta.maintainers = with lib.maintainers; [ ninjatrappeur ];
-  meta.doc = ./pleroma.xml;
+  meta.doc = ./pleroma.md;
 }
diff --git a/nixos/modules/services/networking/pleroma.xml b/nixos/modules/services/networking/pleroma.xml
deleted file mode 100644
index ad0a481af28..00000000000
--- a/nixos/modules/services/networking/pleroma.xml
+++ /dev/null
@@ -1,188 +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-pleroma">
- <title>Pleroma</title>
- <para>
-  <link xlink:href="https://pleroma.social/">Pleroma</link> is a lightweight activity pub server.</para>
- <section xml:id="module-services-pleroma-generate-config">
-  <title>Generating the Pleroma config</title>
-  <para>The <literal>pleroma_ctl</literal> CLI utility will prompt you some questions and it will generate an initial config file. This is an example of usage
-<programlisting>
-<prompt>$ </prompt>mkdir tmp-pleroma
-<prompt>$ </prompt>cd tmp-pleroma
-<prompt>$ </prompt>nix-shell -p pleroma-otp
-<prompt>$ </prompt>pleroma_ctl instance gen --output config.exs --output-psql setup.psql
-</programlisting>
-  </para>
-  <para>The <literal>config.exs</literal> file can be further customized following the instructions on the <link xlink:href="https://docs-develop.pleroma.social/backend/configuration/cheatsheet/">upstream documentation</link>. Many refinements can be applied also after the service is running.</para>
- </section>
- <section xml:id="module-services-pleroma-initialize-db">
-  <title>Initializing the database</title>
-  <para>First, the Postgresql service must be enabled in the NixOS configuration
-<programlisting>
-services.postgresql = {
-  enable = true;
-  package = pkgs.postgresql_13;
-};
-</programlisting>
-and activated with the usual
-<programlisting>
-<prompt>$ </prompt>nixos-rebuild switch
-</programlisting>
-  </para>
-  <para>Then you can create and seed the database, using the <literal>setup.psql</literal> file that you generated in the previous section, by running
-<programlisting>
-<prompt>$ </prompt>sudo -u postgres psql -f setup.psql
-</programlisting>
-  </para>
- </section>
- <section xml:id="module-services-pleroma-enable">
-  <title>Enabling the Pleroma service locally</title>
-  <para>In this section we will enable the Pleroma service only locally, so its configurations can be improved incrementally.</para>
-  <para>This is an example of configuration, where <link linkend="opt-services.pleroma.configs">services.pleroma.configs</link> option contains the content of the file <literal>config.exs</literal>, generated <link linkend="module-services-pleroma-generate-config">in the first section</link>, but with the secrets (database password, endpoint secret key, salts, etc.) removed. Removing secrets is important, because otherwise they will be stored publicly in the Nix store.
-<programlisting>
-services.pleroma = {
-  enable = true;
-  secretConfigFile = "/var/lib/pleroma/secrets.exs";
-  configs = [
-    ''
-    import Config
-
-    config :pleroma, Pleroma.Web.Endpoint,
-      url: [host: "pleroma.example.net", scheme: "https", port: 443],
-      http: [ip: {127, 0, 0, 1}, port: 4000]
-
-    config :pleroma, :instance,
-      name: "Test",
-      email: "admin@example.net",
-      notify_email: "admin@example.net",
-      limit: 5000,
-      registrations_open: true
-
-    config :pleroma, :media_proxy,
-      enabled: false,
-      redirect_on_failure: true
-
-    config :pleroma, Pleroma.Repo,
-      adapter: Ecto.Adapters.Postgres,
-      username: "pleroma",
-      database: "pleroma",
-      hostname: "localhost"
-
-    # Configure web push notifications
-    config :web_push_encryption, :vapid_details,
-      subject: "mailto:admin@example.net"
-
-    # ... TO CONTINUE ...
-    ''
-  ];
-};
-</programlisting>
-  </para>
-  <para>Secrets must be moved into a file pointed by <link linkend="opt-services.pleroma.secretConfigFile">services.pleroma.secretConfigFile</link>, in our case <literal>/var/lib/pleroma/secrets.exs</literal>. This file can be created copying the previously generated <literal>config.exs</literal> file and then removing all the settings, except the secrets. This is an example
-<programlisting>
-# Pleroma instance passwords
-
-import Config
-
-config :pleroma, Pleroma.Web.Endpoint,
-   secret_key_base: "&lt;the secret generated by pleroma_ctl&gt;",
-   signing_salt: "&lt;the secret generated by pleroma_ctl&gt;"
-
-config :pleroma, Pleroma.Repo,
-  password: "&lt;the secret generated by pleroma_ctl&gt;"
-
-# Configure web push notifications
-config :web_push_encryption, :vapid_details,
-  public_key: "&lt;the secret generated by pleroma_ctl&gt;",
-  private_key: "&lt;the secret generated by pleroma_ctl&gt;"
-
-# ... TO CONTINUE ...
-</programlisting>
-  Note that the lines of the same configuration group are comma separated (i.e. all the lines end with a comma, except the last one), so when the lines with passwords are added or removed, commas must be adjusted accordingly.</para>
-
-  <para>The service can be enabled with the usual
-<programlisting>
-<prompt>$ </prompt>nixos-rebuild switch
-</programlisting>
-  </para>
-  <para>The service is accessible only from the local <literal>127.0.0.1:4000</literal> port. It can be tested using a port forwarding like this
-<programlisting>
-<prompt>$ </prompt>ssh -L 4000:localhost:4000 myuser@example.net
-</programlisting>
-and then accessing <link xlink:href="http://localhost:4000">http://localhost:4000</link> from a web browser.</para>
- </section>
- <section xml:id="module-services-pleroma-admin-user">
-  <title>Creating the admin user</title>
-  <para>After Pleroma service is running, all <link xlink:href="https://docs-develop.pleroma.social/">Pleroma administration utilities</link> can be used. In particular an admin user can be created with
-<programlisting>
-<prompt>$ </prompt>pleroma_ctl user new &lt;nickname&gt; &lt;email&gt;  --admin --moderator --password &lt;password&gt;
-</programlisting>
-  </para>
- </section>
- <section xml:id="module-services-pleroma-nginx">
-  <title>Configuring Nginx</title>
-  <para>In this configuration, Pleroma is listening only on the local port 4000. Nginx can be configured as a Reverse Proxy, for forwarding requests from public ports to the Pleroma service. This is an example of configuration, using
-<link xlink:href="https://letsencrypt.org/">Let's Encrypt</link> for the TLS certificates
-<programlisting>
-security.acme = {
-  email = "root@example.net";
-  acceptTerms = true;
-};
-
-services.nginx = {
-  enable = true;
-  addSSL = true;
-
-  recommendedTlsSettings = true;
-  recommendedOptimisation = true;
-  recommendedGzipSettings = true;
-
-  recommendedProxySettings = false;
-  # NOTE: if enabled, the NixOS proxy optimizations will override the Pleroma
-  # specific settings, and they will enter in conflict.
-
-  virtualHosts = {
-    "pleroma.example.net" = {
-      http2 = true;
-      enableACME = true;
-      forceSSL = true;
-
-      locations."/" = {
-        proxyPass = "http://127.0.0.1:4000";
-
-        extraConfig = ''
-          etag on;
-          gzip on;
-
-          add_header 'Access-Control-Allow-Origin' '*' always;
-          add_header 'Access-Control-Allow-Methods' 'POST, PUT, DELETE, GET, PATCH, OPTIONS' always;
-          add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Idempotency-Key' always;
-          add_header 'Access-Control-Expose-Headers' 'Link, X-RateLimit-Reset, X-RateLimit-Limit, X-RateLimit-Remaining, X-Request-Id' always;
-          if ($request_method = OPTIONS) {
-            return 204;
-          }
-          add_header X-XSS-Protection "1; mode=block";
-          add_header X-Permitted-Cross-Domain-Policies none;
-          add_header X-Frame-Options DENY;
-          add_header X-Content-Type-Options nosniff;
-          add_header Referrer-Policy same-origin;
-          add_header X-Download-Options noopen;
-          proxy_http_version 1.1;
-          proxy_set_header Upgrade $http_upgrade;
-          proxy_set_header Connection "upgrade";
-          proxy_set_header Host $host;
-
-          client_max_body_size 16m;
-          # NOTE: increase if users need to upload very big files
-        '';
-      };
-    };
-  };
-};
-</programlisting>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/networking/powerdns.nix b/nixos/modules/services/networking/powerdns.nix
index 6aa5928d637..850a128cf1a 100644
--- a/nixos/modules/services/networking/powerdns.nix
+++ b/nixos/modules/services/networking/powerdns.nix
@@ -5,6 +5,7 @@ with lib;
 let
   cfg = config.services.powerdns;
   configDir = pkgs.writeTextDir "pdns.conf" "${cfg.extraConfig}";
+  finalConfigDir = if cfg.secretFile == null then configDir else "/run/pdns";
 in {
   options = {
     services.powerdns = {
@@ -19,6 +20,19 @@ in {
           for details on supported values.
         '';
       };
+
+      secretFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/keys/powerdns.env";
+        description = lib.mdDoc ''
+          Environment variables from this file will be interpolated into the
+          final config file using envsubst with this syntax: `$ENVIRONMENT`
+          or `''${VARIABLE}`.
+          The file should contain lines formatted as `SECRET_VAR=SECRET_VALUE`.
+          This is useful to avoid putting secrets into the nix store.
+        '';
+      };
     };
   };
 
@@ -31,7 +45,13 @@ in {
       after = [ "network.target" "mysql.service" "postgresql.service" "openldap.service" ];
 
       serviceConfig = {
-        ExecStart = [ "" "${pkgs.pdns}/bin/pdns_server --config-dir=${configDir} --guardian=no --daemon=no --disable-syslog --log-timestamp=no --write-pid=no" ];
+        EnvironmentFile = lib.optional (cfg.secretFile != null) cfg.secretFile;
+        ExecStartPre = lib.optional (cfg.secretFile != null)
+          (pkgs.writeShellScript "pdns-pre-start" ''
+            umask 077
+            ${pkgs.envsubst}/bin/envsubst -i "${configDir}/pdns.conf" > ${finalConfigDir}/pdns.conf
+          '');
+        ExecStart = [ "" "${pkgs.pdns}/bin/pdns_server --config-dir=${finalConfigDir} --guardian=no --daemon=no --disable-syslog --log-timestamp=no --write-pid=no" ];
       };
     };
 
diff --git a/nixos/modules/services/networking/prosody.md b/nixos/modules/services/networking/prosody.md
new file mode 100644
index 00000000000..2da2c242a98
--- /dev/null
+++ b/nixos/modules/services/networking/prosody.md
@@ -0,0 +1,72 @@
+# Prosody {#module-services-prosody}
+
+[Prosody](https://prosody.im/) is an open-source, modern XMPP server.
+
+## Basic usage {#module-services-prosody-basic-usage}
+
+A common struggle for most XMPP newcomers is to find the right set
+of XMPP Extensions (XEPs) to setup. Forget to activate a few of
+those and your XMPP experience might turn into a nightmare!
+
+The XMPP community tackles this problem by creating a meta-XEP
+listing a decent set of XEPs you should implement. This meta-XEP
+is issued every year, the 2020 edition being
+[XEP-0423](https://xmpp.org/extensions/xep-0423.html).
+
+The NixOS Prosody module will implement most of these recommendend XEPs out of
+the box. That being said, two components still require some
+manual configuration: the
+[Multi User Chat (MUC)](https://xmpp.org/extensions/xep-0045.html)
+and the [HTTP File Upload](https://xmpp.org/extensions/xep-0363.html) ones.
+You'll need to create a DNS subdomain for each of those. The current convention is to name your
+MUC endpoint `conference.example.org` and your HTTP upload domain `upload.example.org`.
+
+A good configuration to start with, including a
+[Multi User Chat (MUC)](https://xmpp.org/extensions/xep-0045.html)
+endpoint as well as a [HTTP File Upload](https://xmpp.org/extensions/xep-0363.html)
+endpoint will look like this:
+```
+services.prosody = {
+  enable = true;
+  admins = [ "root@example.org" ];
+  ssl.cert = "/var/lib/acme/example.org/fullchain.pem";
+  ssl.key = "/var/lib/acme/example.org/key.pem";
+  virtualHosts."example.org" = {
+      enabled = true;
+      domain = "example.org";
+      ssl.cert = "/var/lib/acme/example.org/fullchain.pem";
+      ssl.key = "/var/lib/acme/example.org/key.pem";
+  };
+  muc = [ {
+      domain = "conference.example.org";
+  } ];
+  uploadHttp = {
+      domain = "upload.example.org";
+  };
+};
+```
+
+## Let's Encrypt Configuration {#module-services-prosody-letsencrypt}
+
+As you can see in the code snippet from the
+[previous section](#module-services-prosody-basic-usage),
+you'll need a single TLS certificate covering your main endpoint,
+the MUC one as well as the HTTP Upload one. We can generate such a
+certificate by leveraging the ACME
+[extraDomainNames](#opt-security.acme.certs._name_.extraDomainNames) module option.
+
+Provided the setup detailed in the previous section, you'll need the following acme configuration to generate
+a TLS certificate for the three endponits:
+```
+security.acme = {
+  email = "root@example.org";
+  acceptTerms = true;
+  certs = {
+    "example.org" = {
+      webroot = "/var/www/example.org";
+      email = "root@example.org";
+      extraDomainNames = [ "conference.example.org" "upload.example.org" ];
+    };
+  };
+};
+```
diff --git a/nixos/modules/services/networking/prosody.nix b/nixos/modules/services/networking/prosody.nix
index 342638f93ba..9f68853f9fa 100644
--- a/nixos/modules/services/networking/prosody.nix
+++ b/nixos/modules/services/networking/prosody.nix
@@ -904,5 +904,6 @@ in
     };
 
   };
-  meta.doc = ./prosody.xml;
+
+  meta.doc = ./prosody.md;
 }
diff --git a/nixos/modules/services/networking/prosody.xml b/nixos/modules/services/networking/prosody.xml
deleted file mode 100644
index 6358d744ff7..00000000000
--- a/nixos/modules/services/networking/prosody.xml
+++ /dev/null
@@ -1,87 +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-prosody">
- <title>Prosody</title>
- <para>
-  <link xlink:href="https://prosody.im/">Prosody</link> is an open-source, modern XMPP server.
- </para>
- <section xml:id="module-services-prosody-basic-usage">
-  <title>Basic usage</title>
-
-  <para>
-    A common struggle for most XMPP newcomers is to find the right set
-    of XMPP Extensions (XEPs) to setup. Forget to activate a few of
-    those and your XMPP experience might turn into a nightmare!
-  </para>
-
-  <para>
-    The XMPP community tackles this problem by creating a meta-XEP
-    listing a decent set of XEPs you should implement. This meta-XEP
-    is issued every year, the 2020 edition being
-    <link xlink:href="https://xmpp.org/extensions/xep-0423.html">XEP-0423</link>.
-  </para>
-  <para>
-    The NixOS Prosody module will implement most of these recommendend XEPs out of
-    the box. That being said, two components still require some
-    manual configuration: the
-    <link xlink:href="https://xmpp.org/extensions/xep-0045.html">Multi User Chat (MUC)</link>
-    and the <link xlink:href="https://xmpp.org/extensions/xep-0363.html">HTTP File Upload</link> ones.
-    You'll need to create a DNS subdomain for each of those. The current convention is to name your
-    MUC endpoint <literal>conference.example.org</literal> and your HTTP upload domain <literal>upload.example.org</literal>.
-  </para>
-  <para>
-    A good configuration to start with, including a
-    <link xlink:href="https://xmpp.org/extensions/xep-0045.html">Multi User Chat (MUC)</link>
-    endpoint as well as a <link xlink:href="https://xmpp.org/extensions/xep-0363.html">HTTP File Upload</link>
-    endpoint will look like this:
-    <programlisting>
-services.prosody = {
-  <link linkend="opt-services.prosody.enable">enable</link> = true;
-  <link linkend="opt-services.prosody.admins">admins</link> = [ "root@example.org" ];
-  <link linkend="opt-services.prosody.ssl.cert">ssl.cert</link> = "/var/lib/acme/example.org/fullchain.pem";
-  <link linkend="opt-services.prosody.ssl.key">ssl.key</link> = "/var/lib/acme/example.org/key.pem";
-  <link linkend="opt-services.prosody.virtualHosts">virtualHosts</link>."example.org" = {
-      <link linkend="opt-services.prosody.virtualHosts._name_.enabled">enabled</link> = true;
-      <link linkend="opt-services.prosody.virtualHosts._name_.domain">domain</link> = "example.org";
-      <link linkend="opt-services.prosody.virtualHosts._name_.ssl.cert">ssl.cert</link> = "/var/lib/acme/example.org/fullchain.pem";
-      <link linkend="opt-services.prosody.virtualHosts._name_.ssl.key">ssl.key</link> = "/var/lib/acme/example.org/key.pem";
-  };
-  <link linkend="opt-services.prosody.muc">muc</link> = [ {
-      <link linkend="opt-services.prosody.muc">domain</link> = "conference.example.org";
-  } ];
-  <link linkend="opt-services.prosody.uploadHttp">uploadHttp</link> = {
-      <link linkend="opt-services.prosody.uploadHttp.domain">domain</link> = "upload.example.org";
-  };
-};</programlisting>
-  </para>
- </section>
- <section xml:id="module-services-prosody-letsencrypt">
-  <title>Let's Encrypt Configuration</title>
- <para>
-   As you can see in the code snippet from the
-   <link linkend="module-services-prosody-basic-usage">previous section</link>,
-   you'll need a single TLS certificate covering your main endpoint,
-   the MUC one as well as the HTTP Upload one. We can generate such a
-   certificate by leveraging the ACME
-   <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> module option.
- </para>
- <para>
-   Provided the setup detailed in the previous section, you'll need the following acme configuration to generate
-   a TLS certificate for the three endponits:
-    <programlisting>
-security.acme = {
-  <link linkend="opt-security.acme.defaults.email">email</link> = "root@example.org";
-  <link linkend="opt-security.acme.acceptTerms">acceptTerms</link> = true;
-  <link linkend="opt-security.acme.certs">certs</link> = {
-    "example.org" = {
-      <link linkend="opt-security.acme.certs._name_.webroot">webroot</link> = "/var/www/example.org";
-      <link linkend="opt-security.acme.certs._name_.email">email</link> = "root@example.org";
-      <link linkend="opt-security.acme.certs._name_.extraDomainNames">extraDomainNames</link> = [ "conference.example.org" "upload.example.org" ];
-    };
-  };
-};</programlisting>
- </para>
-</section>
-</chapter>
diff --git a/nixos/modules/services/networking/radicale.nix b/nixos/modules/services/networking/radicale.nix
index 9ec507fe2ab..00dbd6bbe38 100644
--- a/nixos/modules/services/networking/radicale.nix
+++ b/nixos/modules/services/networking/radicale.nix
@@ -9,7 +9,7 @@ let
     listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
   };
 
-  pkg = if isNull cfg.package then
+  pkg = if cfg.package == null then
     pkgs.radicale
   else
     cfg.package;
@@ -117,13 +117,13 @@ in {
       }
     ];
 
-    warnings = optional (isNull cfg.package && versionOlder config.system.stateVersion "17.09") ''
+    warnings = optional (cfg.package == null && versionOlder config.system.stateVersion "17.09") ''
       The configuration and storage formats of your existing Radicale
       installation might be incompatible with the newest version.
       For upgrade instructions see
       https://radicale.org/2.1.html#documentation/migration-from-1xx-to-2xx.
       Set services.radicale.package to suppress this warning.
-    '' ++ optional (isNull cfg.package && versionOlder config.system.stateVersion "20.09") ''
+    '' ++ optional (cfg.package == null && versionOlder config.system.stateVersion "20.09") ''
       The configuration format of your existing Radicale installation might be
       incompatible with the newest version.  For upgrade instructions see
       https://github.com/Kozea/Radicale/blob/3.0.6/NEWS.md#upgrade-checklist.
@@ -200,5 +200,5 @@ in {
     };
   };
 
-  meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
+  meta.maintainers = with lib.maintainers; [ infinisil dotlambda ];
 }
diff --git a/nixos/modules/services/networking/redsocks.nix b/nixos/modules/services/networking/redsocks.nix
index 45feb1313c9..30d6a0a6336 100644
--- a/nixos/modules/services/networking/redsocks.nix
+++ b/nixos/modules/services/networking/redsocks.nix
@@ -37,7 +37,7 @@ in
               - stderr
               - file:/path/to/file
               - syslog:FACILITY where FACILITY is any of "daemon", "local0",
-              etc.
+                etc.
           '';
       };
 
@@ -125,6 +125,7 @@ in
               lib.mdDoc ''
                 Way to disclose client IP to the proxy.
                   - "false": do not disclose
+
                 http-connect supports the following ways:
                   - "X-Forwarded-For": add header "X-Forwarded-For: IP"
                   - "Forwarded_ip": add header "Forwarded: for=IP" (see RFC7239)
diff --git a/nixos/modules/services/networking/rpcbind.nix b/nixos/modules/services/networking/rpcbind.nix
index aa04214debb..63c4859fbd0 100644
--- a/nixos/modules/services/networking/rpcbind.nix
+++ b/nixos/modules/services/networking/rpcbind.nix
@@ -14,7 +14,7 @@ with lib;
         type = types.bool;
         default = false;
         description = lib.mdDoc ''
-          Whether to enable `rpcbind', an ONC RPC directory service
+          Whether to enable `rpcbind`, an ONC RPC directory service
           notably used by NFS and NIS, and which can be queried
           using the rpcinfo(1) command. `rpcbind` is a replacement for
           `portmap`.
@@ -35,6 +35,16 @@ with lib;
 
     systemd.services.rpcbind = {
       wantedBy = [ "multi-user.target" ];
+      # rpcbind performs a check for /var/run/rpcbind.lock at startup
+      # and will crash if /var/run isn't present. In the stock NixOS
+      # var.conf tmpfiles configuration file, /var/run is symlinked to
+      # /run, so rpcbind can enter a race condition in which /var/run
+      # isn't symlinked yet but tries to interact with the path, so
+      # controlling the order explicitly here ensures that rpcbind can
+      # start successfully. The `wants` instead of `requires` should
+      # avoid creating a strict/brittle dependency.
+      wants = [ "systemd-tmpfiles-setup.service" ];
+      after = [ "systemd-tmpfiles-setup.service" ];
     };
 
     users.users.rpc = {
diff --git a/nixos/modules/services/networking/shellhub-agent.nix b/nixos/modules/services/networking/shellhub-agent.nix
index ad33c50f9d6..7cce23cb9c4 100644
--- a/nixos/modules/services/networking/shellhub-agent.nix
+++ b/nixos/modules/services/networking/shellhub-agent.nix
@@ -14,7 +14,7 @@ in
 
       enable = mkEnableOption (lib.mdDoc "ShellHub Agent daemon");
 
-      package = mkPackageOption pkgs "shellhub-agent" { };
+      package = mkPackageOptionMD pkgs "shellhub-agent" { };
 
       preferredHostname = mkOption {
         type = types.str;
diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix
index 2e67f8b77c0..19ab3f1aa48 100644
--- a/nixos/modules/services/networking/smokeping.nix
+++ b/nixos/modules/services/networking/smokeping.nix
@@ -42,7 +42,7 @@ let
   configPath = pkgs.writeText "smokeping.conf" configFile;
   cgiHome = pkgs.writeScript "smokeping.fcgi" ''
     #!${pkgs.bash}/bin/bash
-    ${cfg.package}/bin/smokeping_cgi ${configPath}
+    ${cfg.package}/bin/smokeping_cgi /etc/smokeping.conf
   '';
 in
 
@@ -307,6 +307,7 @@ in
           source = "${pkgs.fping}/bin/fping";
         };
     };
+    environment.etc."smokeping.conf".source = configPath;
     environment.systemPackages = [ pkgs.fping ];
     users.users.${cfg.user} = {
       isNormalUser = false;
@@ -327,20 +328,20 @@ in
       # Thus, we need to make `smokepingHome` (which is given to `thttpd -d` below) `755`.
       homeMode = "755";
     };
-    users.groups.${cfg.user} = {};
+    users.groups.${cfg.user} = { };
     systemd.services.smokeping = {
-      requiredBy = [ "multi-user.target"];
+      reloadTriggers = [ configPath ];
+      requiredBy = [ "multi-user.target" ];
       serviceConfig = {
         User = cfg.user;
         Restart = "on-failure";
-        ExecStart = "${cfg.package}/bin/smokeping --config=${configPath} --nodaemon";
+        ExecStart = "${cfg.package}/bin/smokeping --config=/etc/smokeping.conf --nodaemon";
       };
       preStart = ''
         mkdir -m 0755 -p ${smokepingHome}/cache ${smokepingHome}/data
-        rm -f ${smokepingHome}/cropper
-        ln -s ${cfg.package}/htdocs/cropper ${smokepingHome}/cropper
-        rm -f ${smokepingHome}/smokeping.fcgi
-        ln -s ${cgiHome} ${smokepingHome}/smokeping.fcgi
+        ln -sf ${cfg.package}/htdocs/css ${smokepingHome}/css
+        ln -sf ${cfg.package}/htdocs/js ${smokepingHome}/js
+        ln -sf ${cgiHome} ${smokepingHome}/smokeping.fcgi
         ${cfg.package}/bin/smokeping --check --config=${configPath}
         ${cfg.package}/bin/smokeping --static --config=${configPath}
       '';
diff --git a/nixos/modules/services/networking/soju.nix b/nixos/modules/services/networking/soju.nix
index d4c4ca47bc8..7f0ac3e3b8e 100644
--- a/nixos/modules/services/networking/soju.nix
+++ b/nixos/modules/services/networking/soju.nix
@@ -120,5 +120,5 @@ in
     };
   };
 
-  meta.maintainers = with maintainers; [ malvo ];
+  meta.maintainers = with maintainers; [ malte-v ];
 }
diff --git a/nixos/modules/services/networking/ssh/lshd.nix b/nixos/modules/services/networking/ssh/lshd.nix
index 41c4ec2d295..af64969c2fc 100644
--- a/nixos/modules/services/networking/ssh/lshd.nix
+++ b/nixos/modules/services/networking/ssh/lshd.nix
@@ -40,7 +40,7 @@ in
         type = types.listOf types.str;
         description = lib.mdDoc ''
           List of network interfaces where listening for connections.
-          When providing the empty list, `[]', lshd listens on all
+          When providing the empty list, `[]`, lshd listens on all
           network interfaces.
         '';
         example = [ "localhost" "1.2.3.4:443" ];
@@ -169,11 +169,11 @@ in
             else (concatStrings (map (i: "--interface=\"${i}\"")
                                      interfaces))} \
           -h "${hostKey}" \
-          ${if !syslog then "--no-syslog" else ""} \
+          ${optionalString (!syslog) "--no-syslog" } \
           ${if passwordAuthentication then "--password" else "--no-password" } \
           ${if publicKeyAuthentication then "--publickey" else "--no-publickey" } \
           ${if rootLogin then "--root-login" else "--no-root-login" } \
-          ${if loginShell != null then "--login-shell=\"${loginShell}\"" else "" } \
+          ${optionalString (loginShell != null) "--login-shell=\"${loginShell}\"" } \
           ${if srpKeyExchange then "--srp-keyexchange" else "--no-srp-keyexchange" } \
           ${if !tcpForwarding then "--no-tcpip-forward" else "--tcpip-forward"} \
           ${if x11Forwarding then "--x11-forward" else "--no-x11-forward" } \
diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix
index af8200c7e29..89ddf821529 100644
--- a/nixos/modules/services/networking/ssh/sshd.nix
+++ b/nixos/modules/services/networking/ssh/sshd.nix
@@ -12,8 +12,24 @@ let
     then cfgc.package
     else pkgs.buildPackages.openssh;
 
+  # reports boolean as yes / no
+  mkValueStringSshd = with lib; v:
+        if isInt           v then toString v
+        else if isString   v then v
+        else if true  ==   v then "yes"
+        else if false ==   v then "no"
+        else if isList     v then concatStringsSep "," v
+        else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}";
+
+  # dont use the "=" operator
+  settingsFormat = (pkgs.formats.keyValue {
+      mkKeyValue = lib.generators.mkKeyValueDefault {
+      mkValueString = mkValueStringSshd;
+    } " ";});
+
+  configFile = settingsFormat.generate "config" cfg.settings;
   sshconf = pkgs.runCommand "sshd.conf-validated" { nativeBuildInputs = [ validationPackage ]; } ''
-    cat >$out <<EOL
+    cat ${configFile} - >$out <<EOL
     ${cfg.extraConfig}
     EOL
 
@@ -24,6 +40,7 @@ let
   cfg  = config.services.openssh;
   cfgc = config.programs.ssh;
 
+
   nssModulesPath = config.system.nssModules.path;
 
   userOptions = {
@@ -79,9 +96,20 @@ in
 
 {
   imports = [
-    (mkAliasOptionModule [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ])
-    (mkAliasOptionModule [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ])
+    (mkAliasOptionModuleMD [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ])
+    (mkAliasOptionModuleMD [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ])
     (mkRenamedOptionModule [ "services" "openssh" "challengeResponseAuthentication" ] [ "services" "openssh" "kbdInteractiveAuthentication" ])
+
+    (mkRenamedOptionModule [ "services" "openssh" "kbdInteractiveAuthentication" ] [  "services" "openssh" "settings" "KbdInteractiveAuthentication" ])
+    (mkRenamedOptionModule [ "services" "openssh" "passwordAuthentication" ] [  "services" "openssh" "settings" "PasswordAuthentication" ])
+    (mkRenamedOptionModule [ "services" "openssh" "useDns" ] [  "services" "openssh" "settings" "UseDns" ])
+    (mkRenamedOptionModule [ "services" "openssh" "permitRootLogin" ] [  "services" "openssh" "settings" "PermitRootLogin" ])
+    (mkRenamedOptionModule [ "services" "openssh" "logLevel" ] [  "services" "openssh" "settings" "LogLevel" ])
+    (mkRenamedOptionModule [ "services" "openssh" "macs" ] [  "services" "openssh" "settings" "Macs" ])
+    (mkRenamedOptionModule [ "services" "openssh" "ciphers" ] [  "services" "openssh" "settings" "Ciphers" ])
+    (mkRenamedOptionModule [ "services" "openssh" "kexAlgorithms" ] [  "services" "openssh" "settings" "KexAlgorithms" ])
+    (mkRenamedOptionModule [ "services" "openssh" "gatewayPorts" ] [  "services" "openssh" "settings" "GatewayPorts" ])
+    (mkRenamedOptionModule [ "services" "openssh" "forwardX11" ] [  "services" "openssh" "settings" "X11Forwarding" ])
   ];
 
   ###### interface
@@ -109,14 +137,6 @@ in
         '';
       };
 
-      forwardX11 = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Whether to allow X11 connections to be forwarded.
-        '';
-      };
-
       allowSFTP = mkOption {
         type = types.bool;
         default = true;
@@ -145,24 +165,6 @@ in
         '';
       };
 
-      permitRootLogin = mkOption {
-        default = "prohibit-password";
-        type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
-        description = lib.mdDoc ''
-          Whether the root user can login using ssh.
-        '';
-      };
-
-      gatewayPorts = mkOption {
-        type = types.str;
-        default = "no";
-        description = lib.mdDoc ''
-          Specifies whether remote hosts are allowed to connect to
-          ports forwarded for the client.  See
-          {manpage}`sshd_config(5)`.
-        '';
-      };
-
       ports = mkOption {
         type = types.listOf types.port;
         default = [22];
@@ -210,22 +212,6 @@ in
         '';
       };
 
-      passwordAuthentication = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Specifies whether password authentication is allowed.
-        '';
-      };
-
-      kbdInteractiveAuthentication = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc ''
-          Specifies whether keyboard-interactive authentication is allowed.
-        '';
-      };
-
       hostKeys = mkOption {
         type = types.listOf types.attrs;
         default =
@@ -288,84 +274,131 @@ in
         '';
       };
 
-      kexAlgorithms = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "sntrup761x25519-sha512@openssh.com"
-          "curve25519-sha256"
-          "curve25519-sha256@libssh.org"
-          "diffie-hellman-group-exchange-sha256"
-        ];
-        description = lib.mdDoc ''
-          Allowed key exchange algorithms
-
-          Uses the lower bound recommended in both
-          <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
-          and
-          <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
-        '';
-      };
-
-      ciphers = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "chacha20-poly1305@openssh.com"
-          "aes256-gcm@openssh.com"
-          "aes128-gcm@openssh.com"
-          "aes256-ctr"
-          "aes192-ctr"
-          "aes128-ctr"
-        ];
-        description = lib.mdDoc ''
-          Allowed ciphers
 
-          Defaults to recommended settings from both
-          <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
-          and
-          <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
-        '';
-      };
 
-      macs = mkOption {
-        type = types.listOf types.str;
-        default = [
-          "hmac-sha2-512-etm@openssh.com"
-          "hmac-sha2-256-etm@openssh.com"
-          "umac-128-etm@openssh.com"
-          "hmac-sha2-512"
-          "hmac-sha2-256"
-          "umac-128@openssh.com"
-        ];
-        description = lib.mdDoc ''
-          Allowed MACs
+      settings = mkOption {
+        description = lib.mdDoc "Configuration for `sshd_config(5)`.";
+        default = { };
+        example = literalExpression ''{
+          UseDns = true;
+          PasswordAuthentication = false;
+        }'';
+        type = types.submodule ({name, ...}: {
+          freeformType = settingsFormat.type;
+          options = {
+            LogLevel = mkOption {
+              type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
+              default = "INFO"; # upstream default
+              description = lib.mdDoc ''
+                Gives the verbosity level that is used when logging messages from sshd(8). Logging with a DEBUG level
+                violates the privacy of users and is not recommended.
+              '';
+            };
+            UseDns = mkOption {
+              type = types.bool;
+              # apply if cfg.useDns then "yes" else "no"
+              default = false;
+              description = lib.mdDoc ''
+                Specifies whether sshd(8) should look up the remote host name, and to check that the resolved host name for
+                the remote IP address maps back to the very same IP address.
+                If this option is set to no (the default) then only addresses and not host names may be used in
+                ~/.ssh/authorized_keys from and sshd_config Match Host directives.
+              '';
+            };
+            X11Forwarding = mkOption {
+              type = types.bool;
+              default = false;
+              description = lib.mdDoc ''
+                Whether to allow X11 connections to be forwarded.
+              '';
+            };
+            PasswordAuthentication = mkOption {
+              type = types.bool;
+              default = true;
+              description = lib.mdDoc ''
+                Specifies whether password authentication is allowed.
+              '';
+            };
+            PermitRootLogin = mkOption {
+              default = "prohibit-password";
+              type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
+              description = lib.mdDoc ''
+                Whether the root user can login using ssh.
+              '';
+            };
+            KbdInteractiveAuthentication = mkOption {
+              type = types.bool;
+              default = true;
+              description = lib.mdDoc ''
+                Specifies whether keyboard-interactive authentication is allowed.
+              '';
+            };
+            GatewayPorts = mkOption {
+              type = types.str;
+              default = "no";
+              description = lib.mdDoc ''
+                Specifies whether remote hosts are allowed to connect to
+                ports forwarded for the client.  See
+                {manpage}`sshd_config(5)`.
+              '';
+            };
+            KexAlgorithms = mkOption {
+              type = types.listOf types.str;
+              default = [
+                "sntrup761x25519-sha512@openssh.com"
+                "curve25519-sha256"
+                "curve25519-sha256@libssh.org"
+                "diffie-hellman-group-exchange-sha256"
+              ];
+              description = lib.mdDoc ''
+                Allowed key exchange algorithms
 
-          Defaults to recommended settings from both
-          <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
-          and
-          <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
-        '';
-      };
+                Uses the lower bound recommended in both
+                <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
+                and
+                <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
+              '';
+            };
+            Macs = mkOption {
+              type = types.listOf types.str;
+              default = [
+                "hmac-sha2-512-etm@openssh.com"
+                "hmac-sha2-256-etm@openssh.com"
+                "umac-128-etm@openssh.com"
+                "hmac-sha2-512"
+                "hmac-sha2-256"
+                "umac-128@openssh.com"
+              ];
+              description = lib.mdDoc ''
+                Allowed MACs
 
-      logLevel = mkOption {
-        type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
-        default = "INFO"; # upstream default
-        description = lib.mdDoc ''
-          Gives the verbosity level that is used when logging messages from sshd(8). The possible values are:
-          QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG, DEBUG1, DEBUG2, and DEBUG3. The default is INFO. DEBUG and DEBUG1
-          are equivalent. DEBUG2 and DEBUG3 each specify higher levels of debugging output. Logging with a DEBUG level
-          violates the privacy of users and is not recommended.
-        '';
-      };
+                Defaults to recommended settings from both
+                <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
+                and
+                <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
+              '';
+            };
+            Ciphers = mkOption {
+              type = types.listOf types.str;
+              default = [
+                "chacha20-poly1305@openssh.com"
+                "aes256-gcm@openssh.com"
+                "aes128-gcm@openssh.com"
+                "aes256-ctr"
+                "aes192-ctr"
+                "aes128-ctr"
+              ];
+              description = lib.mdDoc ''
+                Allowed ciphers
 
-      useDns = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Specifies whether sshd(8) should look up the remote host name, and to check that the resolved host name for
-          the remote IP address maps back to the very same IP address.
-          If this option is set to no (the default) then only addresses and not host names may be used in
-          ~/.ssh/authorized_keys from and sshd_config Match Host directives.
-        '';
+                Defaults to recommended settings from both
+                <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
+                and
+                <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
+              '';
+            };
+          };
+        });
       };
 
       extraConfig = mkOption {
@@ -441,10 +474,10 @@ in
                       mkdir -m 0755 -p "$(dirname '${k.path}')"
                       ssh-keygen \
                         -t "${k.type}" \
-                        ${if k ? bits then "-b ${toString k.bits}" else ""} \
-                        ${if k ? rounds then "-a ${toString k.rounds}" else ""} \
-                        ${if k ? comment then "-C '${k.comment}'" else ""} \
-                        ${if k ? openSSHFormat && k.openSSHFormat then "-o" else ""} \
+                        ${optionalString (k ? bits) "-b ${toString k.bits}"} \
+                        ${optionalString (k ? rounds) "-a ${toString k.rounds}"} \
+                        ${optionalString (k ? comment) "-C '${k.comment}'"} \
+                        ${optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \
                         -f "${k.path}" \
                         -N ""
                   fi
@@ -496,14 +529,14 @@ in
     security.pam.services.sshd =
       { startSession = true;
         showMotd = true;
-        unixAuth = cfg.passwordAuthentication;
+        unixAuth = cfg.settings.PasswordAuthentication;
       };
 
     # These values are merged with the ones defined externally, see:
     # https://github.com/NixOS/nixpkgs/pull/10155
     # https://github.com/NixOS/nixpkgs/pull/41745
     services.openssh.authorizedKeysFiles =
-      [ "%h/.ssh/authorized_keys" "%h/.ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
+      [ "%h/.ssh/authorized_keys" "/etc/ssh/authorized_keys.d/%u" ];
 
     services.openssh.extraConfig = mkOrder 0
       ''
@@ -517,26 +550,16 @@ in
         '') cfg.ports}
 
         ${concatMapStrings ({ port, addr, ... }: ''
-          ListenAddress ${addr}${if port != null then ":" + toString port else ""}
+          ListenAddress ${addr}${optionalString (port != null) (":" + toString port)}
         '') cfg.listenAddresses}
 
         ${optionalString cfgc.setXAuthLocation ''
             XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
         ''}
-
-        X11Forwarding ${if cfg.forwardX11 then "yes" else "no"}
-
         ${optionalString cfg.allowSFTP ''
           Subsystem sftp ${cfg.sftpServerExecutable} ${concatStringsSep " " cfg.sftpFlags}
         ''}
-
-        PermitRootLogin ${cfg.permitRootLogin}
-        GatewayPorts ${cfg.gatewayPorts}
-        PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"}
-        KbdInteractiveAuthentication ${if cfg.kbdInteractiveAuthentication then "yes" else "no"}
-
         PrintMotd no # handled by pam_motd
-
         AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
         ${optionalString (cfg.authorizedKeysCommand != "none") ''
           AuthorizedKeysCommand ${cfg.authorizedKeysCommand}
@@ -546,18 +569,9 @@ in
         ${flip concatMapStrings cfg.hostKeys (k: ''
           HostKey ${k.path}
         '')}
-
-        KexAlgorithms ${concatStringsSep "," cfg.kexAlgorithms}
-        Ciphers ${concatStringsSep "," cfg.ciphers}
-        MACs ${concatStringsSep "," cfg.macs}
-
-        LogLevel ${cfg.logLevel}
-
-        UseDNS ${if cfg.useDns then "yes" else "no"}
-
       '';
 
-    assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true;
+    assertions = [{ assertion = if cfg.settings.X11Forwarding then cfgc.setXAuthLocation else true;
                     message = "cannot enable X11 forwarding without setting xauth location";}]
       ++ forEach cfg.listenAddresses ({ addr, ... }: {
         assertion = addr != null;
diff --git a/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix b/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
index 84ac4fef26e..1ad5fdbcef0 100644
--- a/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
+++ b/nixos/modules/services/networking/strongswan-swanctl/swanctl-params.nix
@@ -225,20 +225,22 @@ in {
       irrespective of the value of this option (even when set to no).
     '';
 
-    childless = mkEnumParam [ "allow" "force" "never" ] "allow" ''
-      Use childless IKE_SA initiation (RFC 6023) for IKEv2.  Acceptable values
-      are `allow` (the default), `force` and
-      `never`. If set to `allow`, responders
+    childless = mkEnumParam [ "allow" "prefer" "force" "never" ] "allow" ''
+      Use childless IKE_SA initiation (_allow_, _prefer_, _force_ or _never_).
+
+      Use childless IKE_SA initiation (RFC 6023) for IKEv2, with the first
+      CHILD_SA created with a separate CREATE_CHILD_SA exchange (e.g. to use an
+      independent DH exchange for all CHILD_SAs).  Acceptable values are `allow`
+      (the default), `prefer`, `force` and `never`. If set to `allow`, responders
       will accept childless IKE_SAs (as indicated via notify in the IKE_SA_INIT
-      response) while initiators continue to create regular IKE_SAs with the
-      first CHILD_SA created during IKE_AUTH, unless the IKE_SA is initiated
-      explicitly without any children (which will fail if the responder does not
-      support or has disabled this extension).  If set to
-      `force`, only childless initiation is accepted and the
-      first CHILD_SA is created with a separate CREATE_CHILD_SA exchange
-      (e.g. to use an independent DH exchange for all CHILD_SAs). Finally,
-      setting the option to `never` disables support for
-      childless IKE_SAs as responder.
+      response) while initiators continue to create regular IKE_SAs with the first
+      CHILD_SA created during IKE_AUTH, unless the IKE_SA is initiated explicitly
+      without any children (which will fail if the responder does not support or
+      has disabled this extension). The effect of `prefer` is the same as `allow`
+      on responders, but as initiator a childless IKE_SA is initiated if the
+      responder supports it. If set to `force`, only childless initiation is
+      accepted in either role.  Finally, setting the option to `never` disables
+      support for childless IKE_SAs as responder.
     '';
 
     send_certreq = mkYesNoParam yes ''
@@ -357,11 +359,22 @@ in {
     if_id_in = mkStrParam "0" ''
       XFRM interface ID set on inbound policies/SA, can be overridden by child
       config, see there for details.
+
+      The special value `%unique` allocates a unique interface ID per IKE_SA,
+      which is inherited by all its CHILD_SAs (unless overridden there), beyond
+      that the value `%unique-dir` assigns a different unique interface ID for
+      each direction (in/out).
+
     '';
 
     if_id_out = mkStrParam "0" ''
       XFRM interface ID set on outbound policies/SA, can be overridden by child
       config, see there for details.
+
+      The special value `%unique` allocates a unique interface ID per IKE_SA,
+      which is inherited by all its CHILD_SAs (unless overridden there), beyond
+      that the value `%unique-dir` assigns a different unique interface ID for
+      each direction (in/out).
     '';
 
     mediation = mkYesNoParam no ''
@@ -985,12 +998,14 @@ in {
         protection.
       '';
 
-      hw_offload = mkEnumParam ["yes" "no" "auto"] "no" ''
+      hw_offload = mkEnumParam ["yes" "no" "auto" "crypto" "packet"] "no" ''
         Enable hardware offload for this CHILD_SA, if supported by the IPsec
-        implementation. The value `yes` enforces offloading
-        and the installation will fail if it's not supported by either kernel or
-        device. The value `auto` enables offloading, if it's
-        supported, but the installation does not fail otherwise.
+        implementation. The values `crypto` or `packet` enforce crypto or full
+        packet offloading and the installation will fail if the selected mode is not
+        supported by either kernel or device. On Linux, `packet` also offloads
+        policies, including trap policies. The value `auto` enables full packet
+        or crypto offloading, if either is supported, but the installation does not
+        fail otherwise.
       '';
 
       copy_df = mkYesNoParam yes ''
diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix
index 8b1398bfd47..e58526814d1 100644
--- a/nixos/modules/services/networking/strongswan.nix
+++ b/nixos/modules/services/networking/strongswan.nix
@@ -4,7 +4,7 @@ let
 
   inherit (builtins) toFile;
   inherit (lib) concatMapStringsSep concatStringsSep mapAttrsToList
-                mkIf mkEnableOption mkOption types literalExpression;
+                mkIf mkEnableOption mkOption types literalExpression optionalString;
 
   cfg = config.services.strongswan;
 
@@ -34,8 +34,8 @@ let
 
   strongswanConf = {setup, connections, ca, secretsFile, managePlugins, enabledPlugins}: toFile "strongswan.conf" ''
     charon {
-      ${if managePlugins then "load_modular = no" else ""}
-      ${if managePlugins then ("load = " + (concatStringsSep " " enabledPlugins)) else ""}
+      ${optionalString managePlugins "load_modular = no"}
+      ${optionalString managePlugins ("load = " + (concatStringsSep " " enabledPlugins))}
       plugins {
         stroke {
           secrets_file = ${secretsFile}
diff --git a/nixos/modules/services/networking/stunnel.nix b/nixos/modules/services/networking/stunnel.nix
index 4f592fb312d..996e9b22539 100644
--- a/nixos/modules/services/networking/stunnel.nix
+++ b/nixos/modules/services/networking/stunnel.nix
@@ -154,8 +154,8 @@ in
     environment.systemPackages = [ pkgs.stunnel ];
 
     environment.etc."stunnel.cfg".text = ''
-      ${ if cfg.user != null then "setuid = ${cfg.user}" else "" }
-      ${ if cfg.group != null then "setgid = ${cfg.group}" else "" }
+      ${ optionalString (cfg.user != null) "setuid = ${cfg.user}" }
+      ${ optionalString (cfg.group != null) "setgid = ${cfg.group}" }
 
       debug = ${cfg.logLevel}
 
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index adbb25ccb9b..3d41fe4013e 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -384,6 +384,29 @@ in {
         description = mdDoc ''
           Extra configuration options for Syncthing.
           See <https://docs.syncthing.net/users/config.html>.
+          Note that this attribute set does not exactly match the documented
+          xml format. Instead, this is the format of the json rest api. There
+          are slight differences. For example, this xml:
+          ```xml
+          <options>
+            <listenAddress>default</listenAddress>
+            <minHomeDiskFree unit="%">1</minHomeDiskFree>
+          </options>
+          ```
+          corresponds to the json:
+          ```json
+          {
+            options: {
+              listenAddresses = [
+                "default"
+              ];
+              minHomeDiskFree = {
+                unit = "%";
+                value = 1;
+              };
+            };
+          }
+          ```
         '';
         example = {
           options.localAnnounceEnabled = false;
diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix
index 233bfdf9ebf..c81cf293ab6 100644
--- a/nixos/modules/services/networking/tailscale.nix
+++ b/nixos/modules/services/networking/tailscale.nix
@@ -82,8 +82,8 @@ in {
     };
 
     boot.kernel.sysctl = mkIf (cfg.useRoutingFeatures == "server" || cfg.useRoutingFeatures == "both") {
-      "net.ipv4.conf.all.forwarding" = mkDefault true;
-      "net.ipv6.conf.all.forwarding" = mkDefault true;
+      "net.ipv4.conf.all.forwarding" = mkOverride 97 true;
+      "net.ipv6.conf.all.forwarding" = mkOverride 97 true;
     };
 
     networking.firewall.checkReversePath = mkIf (cfg.useRoutingFeatures == "client" || cfg.useRoutingFeatures == "both") "loose";
diff --git a/nixos/modules/services/networking/teleport.nix b/nixos/modules/services/networking/teleport.nix
index 6433554f87d..399af711c0e 100644
--- a/nixos/modules/services/networking/teleport.nix
+++ b/nixos/modules/services/networking/teleport.nix
@@ -11,6 +11,14 @@ in
     services.teleport = with lib.types; {
       enable = mkEnableOption (lib.mdDoc "the Teleport service");
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.teleport;
+        defaultText = lib.literalMD "pkgs.teleport";
+        example = lib.literalMD "pkgs.teleport_11";
+        description = lib.mdDoc "The teleport package to use";
+      };
+
       settings = mkOption {
         type = settingsYaml.type;
         default = { };
@@ -74,14 +82,14 @@ in
   };
 
   config = mkIf config.services.teleport.enable {
-    environment.systemPackages = [ pkgs.teleport ];
+    environment.systemPackages = [ cfg.package ];
 
     systemd.services.teleport = {
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
       serviceConfig = {
         ExecStart = ''
-          ${pkgs.teleport}/bin/teleport start \
+          ${cfg.package}/bin/teleport start \
             ${optionalString cfg.insecure.enable "--insecure"} \
             ${optionalString cfg.diag.enable "--diag-addr=${cfg.diag.addr}:${toString cfg.diag.port}"} \
             ${optionalString (cfg.settings != { }) "--config=${settingsYaml.generate "teleport.yaml" cfg.settings}"}
diff --git a/nixos/modules/services/networking/tinc.nix b/nixos/modules/services/networking/tinc.nix
index 09b23a60a4a..7db83e6a584 100644
--- a/nixos/modules/services/networking/tinc.nix
+++ b/nixos/modules/services/networking/tinc.nix
@@ -349,91 +349,94 @@ in
 
   ###### implementation
 
-  config = mkIf (cfg.networks != { }) {
-
-    environment.etc = foldr (a: b: a // b) { }
-      (flip mapAttrsToList cfg.networks (network: data:
-        flip mapAttrs' data.hosts (host: text: nameValuePair
-          ("tinc/${network}/hosts/${host}")
-          ({ mode = "0644"; user = "tinc.${network}"; inherit text; })
-        ) // {
-          "tinc/${network}/tinc.conf" = {
-            mode = "0444";
-            text = ''
-              ${toTincConf ({ Interface = "tinc.${network}"; } // data.settings)}
-              ${data.extraConfig}
-            '';
+  config = mkIf (cfg.networks != { }) (
+    let
+      etcConfig = foldr (a: b: a // b) { }
+        (flip mapAttrsToList cfg.networks (network: data:
+          flip mapAttrs' data.hosts (host: text: nameValuePair
+            ("tinc/${network}/hosts/${host}")
+            ({ mode = "0644"; user = "tinc.${network}"; inherit text; })
+          ) // {
+            "tinc/${network}/tinc.conf" = {
+              mode = "0444";
+              text = ''
+                ${toTincConf ({ Interface = "tinc.${network}"; } // data.settings)}
+                ${data.extraConfig}
+              '';
+            };
+          }
+        ));
+    in {
+      environment.etc = etcConfig;
+
+      systemd.services = flip mapAttrs' cfg.networks (network: data: nameValuePair
+        ("tinc.${network}")
+        (let version = getVersion data.package; in {
+          description = "Tinc Daemon - ${network}";
+          wantedBy = [ "multi-user.target" ];
+          path = [ data.package ];
+          reloadTriggers = mkIf (versionAtLeast version "1.1pre") [ (builtins.toJSON etcConfig) ];
+          restartTriggers = mkIf (versionOlder version "1.1pre") [ (builtins.toJSON etcConfig) ];
+          serviceConfig = {
+            Type = "simple";
+            Restart = "always";
+            RestartSec = "3";
+            ExecReload = mkIf (versionAtLeast version "1.1pre") "${data.package}/bin/tinc -n ${network} reload";
+            ExecStart = "${data.package}/bin/tincd -D -U tinc.${network} -n ${network} ${optionalString (data.chroot) "-R"} --pidfile /run/tinc.${network}.pid -d ${toString data.debugLevel}";
           };
-        }
-      ));
-
-    systemd.services = flip mapAttrs' cfg.networks (network: data: nameValuePair
-      ("tinc.${network}")
-      ({
-        description = "Tinc Daemon - ${network}";
-        wantedBy = [ "multi-user.target" ];
-        path = [ data.package ];
-        restartTriggers = [ config.environment.etc."tinc/${network}/tinc.conf".source ];
-        serviceConfig = {
-          Type = "simple";
-          Restart = "always";
-          RestartSec = "3";
-          ExecReload = mkIf (versionAtLeast (getVersion data.package) "1.1pre") "${data.package}/bin/tinc -n ${network} reload";
-          ExecStart = "${data.package}/bin/tincd -D -U tinc.${network} -n ${network} ${optionalString (data.chroot) "-R"} --pidfile /run/tinc.${network}.pid -d ${toString data.debugLevel}";
+          preStart = ''
+            mkdir -p /etc/tinc/${network}/hosts
+            chown tinc.${network} /etc/tinc/${network}/hosts
+            mkdir -p /etc/tinc/${network}/invitations
+            chown tinc.${network} /etc/tinc/${network}/invitations
+
+            # Determine how we should generate our keys
+            if type tinc >/dev/null 2>&1; then
+              # Tinc 1.1+ uses the tinc helper application for key generation
+            ${if data.ed25519PrivateKeyFile != null then "  # ed25519 Keyfile managed by nix" else ''
+              # Prefer ED25519 keys (only in 1.1+)
+              [ -f "/etc/tinc/${network}/ed25519_key.priv" ] || tinc -n ${network} generate-ed25519-keys
+            ''}
+            ${if data.rsaPrivateKeyFile != null then "  # RSA Keyfile managed by nix" else ''
+              [ -f "/etc/tinc/${network}/rsa_key.priv" ] || tinc -n ${network} generate-rsa-keys 4096
+            ''}
+              # In case there isn't anything to do
+              true
+            else
+              # Tinc 1.0 uses the tincd application
+              [ -f "/etc/tinc/${network}/rsa_key.priv" ] || tincd -n ${network} -K 4096
+            fi
+          '';
+        })
+      );
+
+      environment.systemPackages = let
+        cli-wrappers = pkgs.stdenv.mkDerivation {
+          name = "tinc-cli-wrappers";
+          nativeBuildInputs = [ pkgs.makeWrapper ];
+          buildCommand = ''
+            mkdir -p $out/bin
+            ${concatStringsSep "\n" (mapAttrsToList (network: data:
+              optionalString (versionAtLeast data.package.version "1.1pre") ''
+                makeWrapper ${data.package}/bin/tinc "$out/bin/tinc.${network}" \
+                  --add-flags "--pidfile=/run/tinc.${network}.pid" \
+                  --add-flags "--config=/etc/tinc/${network}"
+              '') cfg.networks)}
+          '';
         };
-        preStart = ''
-          mkdir -p /etc/tinc/${network}/hosts
-          chown tinc.${network} /etc/tinc/${network}/hosts
-          mkdir -p /etc/tinc/${network}/invitations
-          chown tinc.${network} /etc/tinc/${network}/invitations
-
-          # Determine how we should generate our keys
-          if type tinc >/dev/null 2>&1; then
-            # Tinc 1.1+ uses the tinc helper application for key generation
-          ${if data.ed25519PrivateKeyFile != null then "  # ed25519 Keyfile managed by nix" else ''
-            # Prefer ED25519 keys (only in 1.1+)
-            [ -f "/etc/tinc/${network}/ed25519_key.priv" ] || tinc -n ${network} generate-ed25519-keys
-          ''}
-          ${if data.rsaPrivateKeyFile != null then "  # RSA Keyfile managed by nix" else ''
-            [ -f "/etc/tinc/${network}/rsa_key.priv" ] || tinc -n ${network} generate-rsa-keys 4096
-          ''}
-            # In case there isn't anything to do
-            true
-          else
-            # Tinc 1.0 uses the tincd application
-            [ -f "/etc/tinc/${network}/rsa_key.priv" ] || tincd -n ${network} -K 4096
-          fi
-        '';
-      })
-    );
-
-    environment.systemPackages = let
-      cli-wrappers = pkgs.stdenv.mkDerivation {
-        name = "tinc-cli-wrappers";
-        nativeBuildInputs = [ pkgs.makeWrapper ];
-        buildCommand = ''
-          mkdir -p $out/bin
-          ${concatStringsSep "\n" (mapAttrsToList (network: data:
-            optionalString (versionAtLeast data.package.version "1.1pre") ''
-              makeWrapper ${data.package}/bin/tinc "$out/bin/tinc.${network}" \
-                --add-flags "--pidfile=/run/tinc.${network}.pid" \
-                --add-flags "--config=/etc/tinc/${network}"
-            '') cfg.networks)}
-        '';
-      };
-    in [ cli-wrappers ];
-
-    users.users = flip mapAttrs' cfg.networks (network: _:
-      nameValuePair ("tinc.${network}") ({
-        description = "Tinc daemon user for ${network}";
-        isSystemUser = true;
-        group = "tinc.${network}";
-      })
-    );
-    users.groups = flip mapAttrs' cfg.networks (network: _:
-      nameValuePair "tinc.${network}" {}
-    );
-  };
+      in [ cli-wrappers ];
+
+      users.users = flip mapAttrs' cfg.networks (network: _:
+        nameValuePair ("tinc.${network}") ({
+          description = "Tinc daemon user for ${network}";
+          isSystemUser = true;
+          group = "tinc.${network}";
+        })
+      );
+      users.groups = flip mapAttrs' cfg.networks (network: _:
+        nameValuePair "tinc.${network}" {}
+      );
+    });
 
   meta.maintainers = with maintainers; [ minijackson mic92 ];
 }
diff --git a/nixos/modules/services/networking/tmate-ssh-server.nix b/nixos/modules/services/networking/tmate-ssh-server.nix
index f7740b1ddfc..ff4ce077330 100644
--- a/nixos/modules/services/networking/tmate-ssh-server.nix
+++ b/nixos/modules/services/networking/tmate-ssh-server.nix
@@ -28,7 +28,7 @@ in
     host = mkOption {
       type = types.str;
       description = mdDoc "External host name";
-      defaultText = lib.literalExpression "config.networking.domain or config.networking.hostName ";
+      defaultText = lib.literalExpression "config.networking.domain or config.networking.hostName";
       default =
         if domain == null then
           config.networking.hostName
diff --git a/nixos/modules/services/networking/tox-node.nix b/nixos/modules/services/networking/tox-node.nix
index fa5b241f918..884fd55dae5 100644
--- a/nixos/modules/services/networking/tox-node.nix
+++ b/nixos/modules/services/networking/tox-node.nix
@@ -8,7 +8,7 @@ let
   homeDir = "/var/lib/tox-node";
 
   configFile = let
-    src = "${pkg.src}/dpkg/config.yml";
+    src = "${pkg.src}/tox_node/dpkg/config.yml";
     confJSON = pkgs.writeText "config.json" (
       builtins.toJSON {
         log-type = cfg.logType;
diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix
index c85dd03867f..0426dbb0c83 100644
--- a/nixos/modules/services/networking/unbound.nix
+++ b/nixos/modules/services/networking/unbound.nix
@@ -286,6 +286,8 @@ in {
         LockPersonality = true;
         RestrictSUIDSGID = true;
 
+        ReadWritePaths = [ cfg.stateDir ];
+
         Restart = "on-failure";
         RestartSec = "5s";
       };
diff --git a/nixos/modules/services/networking/unifi.nix b/nixos/modules/services/networking/unifi.nix
index d220aa9fbbe..73adf5572b3 100644
--- a/nixos/modules/services/networking/unifi.nix
+++ b/nixos/modules/services/networking/unifi.nix
@@ -33,8 +33,8 @@ in
 
     services.unifi.unifiPackage = mkOption {
       type = types.package;
-      default = pkgs.unifiLTS;
-      defaultText = literalExpression "pkgs.unifiLTS";
+      default = pkgs.unifi5;
+      defaultText = literalExpression "pkgs.unifi5";
       description = lib.mdDoc ''
         The unifi package to use.
       '';
@@ -42,10 +42,10 @@ in
 
     services.unifi.mongodbPackage = mkOption {
       type = types.package;
-      default = pkgs.mongodb;
+      default = pkgs.mongodb-4_2;
       defaultText = literalExpression "pkgs.mongodb";
       description = lib.mdDoc ''
-        The mongodb package to use.
+        The mongodb package to use. Please note: unifi7 officially only supports mongodb up until 3.6 but works with 4.2.
       '';
     };
 
@@ -194,5 +194,5 @@ in
     (mkRenamedOptionModule [ "services" "unifi" "openPorts" ] [ "services" "unifi" "openFirewall" ])
   ];
 
-  meta.maintainers = with lib.maintainers; [ erictapen pennae ];
+  meta.maintainers = with lib.maintainers; [ pennae ];
 }
diff --git a/nixos/modules/services/networking/v2raya.nix b/nixos/modules/services/networking/v2raya.nix
index 2d697b4fb56..0bea73798da 100644
--- a/nixos/modules/services/networking/v2raya.nix
+++ b/nixos/modules/services/networking/v2raya.nix
@@ -12,27 +12,38 @@ with lib;
   config = mkIf config.services.v2raya.enable {
     environment.systemPackages = [ pkgs.v2raya ];
 
-    systemd.services.v2raya = {
-      unitConfig = {
-        Description = "v2rayA service";
-        Documentation = "https://github.com/v2rayA/v2rayA/wiki";
-        After = [ "network.target" "nss-lookup.target" "iptables.service" "ip6tables.service" ];
-        Wants = [ "network.target" ];
-      };
+    systemd.services.v2raya =
+      let
+        nftablesEnabled = config.networking.nftables.enable;
+        iptablesServices = [
+          "iptables.service"
+        ] ++ optional config.networking.enableIPv6 "ip6tables.service";
+        tableServices = if nftablesEnabled then [ "nftables.service" ] else iptablesServices;
+      in
+      {
+        unitConfig = {
+          Description = "v2rayA service";
+          Documentation = "https://github.com/v2rayA/v2rayA/wiki";
+          After = [
+            "network.target"
+            "nss-lookup.target"
+          ] ++ tableServices;
+          Wants = [ "network.target" ];
+        };
 
-      serviceConfig = {
-        User = "root";
-        ExecStart = "${getExe pkgs.v2raya} --log-disable-timestamp";
-        Environment = [ "V2RAYA_LOG_FILE=/var/log/v2raya/v2raya.log" ];
-        LimitNPROC = 500;
-        LimitNOFILE = 1000000;
-        Restart = "on-failure";
-        Type = "simple";
-      };
+        serviceConfig = {
+          User = "root";
+          ExecStart = "${getExe pkgs.v2raya} --log-disable-timestamp";
+          Environment = [ "V2RAYA_LOG_FILE=/var/log/v2raya/v2raya.log" ];
+          LimitNPROC = 500;
+          LimitNOFILE = 1000000;
+          Restart = "on-failure";
+          Type = "simple";
+        };
 
-      wantedBy = [ "multi-user.target" ];
-      path = with pkgs; [ iptables bash iproute2 ]; # required by v2rayA TProxy functionality
-    };
+        wantedBy = [ "multi-user.target" ];
+        path = with pkgs; [ iptables bash iproute2 ]; # required by v2rayA TProxy functionality
+      };
   };
 
   meta.maintainers = with maintainers; [ elliot ];
diff --git a/nixos/modules/services/networking/vdirsyncer.nix b/nixos/modules/services/networking/vdirsyncer.nix
index 6a069943434..f9b880c763e 100644
--- a/nixos/modules/services/networking/vdirsyncer.nix
+++ b/nixos/modules/services/networking/vdirsyncer.nix
@@ -71,7 +71,7 @@ in
     services.vdirsyncer = {
       enable = mkEnableOption (mdDoc "vdirsyncer");
 
-      package = mkPackageOption pkgs "vdirsyncer" {};
+      package = mkPackageOptionMD pkgs "vdirsyncer" {};
 
       jobs = mkOption {
         description = mdDoc "vdirsyncer job configurations";
diff --git a/nixos/modules/services/networking/webhook.nix b/nixos/modules/services/networking/webhook.nix
new file mode 100644
index 00000000000..2a78491941c
--- /dev/null
+++ b/nixos/modules/services/networking/webhook.nix
@@ -0,0 +1,214 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.webhook;
+  defaultUser = "webhook";
+
+  hookFormat = pkgs.formats.json {};
+
+  hookType = types.submodule ({ name, ... }: {
+    freeformType = hookFormat.type;
+    options = {
+      id = mkOption {
+        type = types.str;
+        default = name;
+        description = mdDoc ''
+          The ID of your hook. This value is used to create the HTTP endpoint (`protocol://yourserver:port/prefix/''${id}`).
+        '';
+      };
+      execute-command = mkOption {
+        type = types.str;
+        description = mdDoc "The command that should be executed when the hook is triggered.";
+      };
+    };
+  });
+
+  hookFiles = mapAttrsToList (name: hook: hookFormat.generate "webhook-${name}.json" [ hook ]) cfg.hooks
+           ++ mapAttrsToList (name: hook: pkgs.writeText "webhook-${name}.json.tmpl" "[${hook}]") cfg.hooksTemplated;
+
+in {
+  options = {
+    services.webhook = {
+      enable = mkEnableOption (mdDoc ''
+        [Webhook](https://github.com/adnanh/webhook), a server written in Go that allows you to create HTTP endpoints (hooks),
+        which execute configured commands for any person or service that knows the URL
+      '');
+
+      package = mkPackageOptionMD pkgs "webhook" {};
+      user = mkOption {
+        type = types.str;
+        default = defaultUser;
+        description = mdDoc ''
+          Webhook will be run under this user.
+
+          If set, you must create this user yourself!
+        '';
+      };
+      group = mkOption {
+        type = types.str;
+        default = defaultUser;
+        description = mdDoc ''
+          Webhook will be run under this group.
+
+          If set, you must create this group yourself!
+        '';
+      };
+      ip = mkOption {
+        type = types.str;
+        default = "0.0.0.0";
+        description = mdDoc ''
+          The IP webhook should serve hooks on.
+
+          The default means it can be reached on any interface if `openFirewall = true`.
+        '';
+      };
+      port = mkOption {
+        type = types.port;
+        default = 9000;
+        description = mdDoc "The port webhook should be reachable from.";
+      };
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Open the configured port in the firewall for external ingress traffic.
+          Preferably the Webhook server is instead put behind a reverse proxy.
+        '';
+      };
+      enableTemplates = mkOption {
+        type = types.bool;
+        default = cfg.hooksTemplated != {};
+        defaultText = literalExpression "hooksTemplated != {}";
+        description = mdDoc ''
+          Enable the generated hooks file to be parsed as a Go template.
+          See [the documentation](https://github.com/adnanh/webhook/blob/master/docs/Templates.md) for more information.
+        '';
+      };
+      urlPrefix = mkOption {
+        type = types.str;
+        default = "hooks";
+        description = mdDoc ''
+          The URL path prefix to use for served hooks (`protocol://yourserver:port/''${prefix}/hook-id`).
+        '';
+      };
+      hooks = mkOption {
+        type = types.attrsOf hookType;
+        default = {};
+        example = {
+          echo = {
+            execute-command = "echo";
+            response-message = "Webhook is reachable!";
+          };
+          redeploy-webhook = {
+            execute-command = "/var/scripts/redeploy.sh";
+            command-working-directory = "/var/webhook";
+          };
+        };
+        description = mdDoc ''
+          The actual configuration of which hooks will be served.
+
+          Read more on the [project homepage] and on the [hook definition] page.
+          At least one hook needs to be configured.
+
+          [hook definition]: https://github.com/adnanh/webhook/blob/master/docs/Hook-Definition.md
+          [project homepage]: https://github.com/adnanh/webhook#configuration
+        '';
+      };
+      hooksTemplated = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        example = {
+          echo-template = ''
+            {
+              "id": "echo-template",
+              "execute-command": "echo",
+              "response-message": "{{ getenv "MESSAGE" }}"
+            }
+          '';
+        };
+        description = mdDoc ''
+          Same as {option}`hooks`, but these hooks are specified as literal strings instead of Nix values,
+          and hence can include [template syntax](https://github.com/adnanh/webhook/blob/master/docs/Templates.md)
+          which might not be representable as JSON.
+
+          Template syntax requires the {option}`enableTemplates` option to be set to `true`, which is
+          done by default if this option is set.
+        '';
+      };
+      verbose = mkOption {
+        type = types.bool;
+        default = true;
+        description = mdDoc "Whether to show verbose output.";
+      };
+      extraArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-secure" ];
+        description = mdDoc ''
+          These are arguments passed to the webhook command in the systemd service.
+          You can find the available arguments and options in the [documentation][parameters].
+
+          [parameters]: https://github.com/adnanh/webhook/blob/master/docs/Webhook-Parameters.md
+        '';
+      };
+      environment = mkOption {
+        type = types.attrsOf types.str;
+        default = {};
+        description = mdDoc "Extra environment variables passed to webhook.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = let
+      overlappingHooks = builtins.intersectAttrs cfg.hooks cfg.hooksTemplated;
+    in [
+      {
+        assertion = hookFiles != [];
+        message = "At least one hook needs to be configured for webhook to run.";
+      }
+      {
+        assertion = overlappingHooks == {};
+        message = "`services.webhook.hooks` and `services.webhook.hooksTemplated` have overlapping attribute(s): ${concatStringsSep ", " (builtins.attrNames overlappingHooks)}";
+      }
+    ];
+
+    users.users = mkIf (cfg.user == defaultUser) {
+      ${defaultUser} =
+        {
+          isSystemUser = true;
+          group = cfg.group;
+          description = "Webhook daemon user";
+        };
+    };
+
+    users.groups = mkIf (cfg.user == defaultUser && cfg.group == defaultUser) {
+      ${defaultUser} = {};
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
+
+    systemd.services.webhook = {
+      description = "Webhook service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = config.networking.proxy.envVars // cfg.environment;
+      script = let
+        args = [ "-ip" cfg.ip "-port" (toString cfg.port) "-urlprefix" cfg.urlPrefix ]
+            ++ concatMap (hook: [ "-hooks" hook ]) hookFiles
+            ++ optional cfg.enableTemplates "-template"
+            ++ optional cfg.verbose "-verbose"
+            ++ cfg.extraArgs;
+      in ''
+        ${cfg.package}/bin/webhook ${escapeShellArgs args}
+      '';
+      serviceConfig = {
+        Restart = "on-failure";
+        User = cfg.user;
+        Group = cfg.group;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/networking/wg-quick.nix b/nixos/modules/services/networking/wg-quick.nix
index a678d743bb7..34210580f53 100644
--- a/nixos/modules/services/networking/wg-quick.nix
+++ b/nixos/modules/services/networking/wg-quick.nix
@@ -273,7 +273,11 @@ let
         after = [ "network.target" "network-online.target" ];
         wantedBy = optional values.autostart "multi-user.target";
         environment.DEVICE = name;
-        path = [ pkgs.kmod pkgs.wireguard-tools config.networking.resolvconf.package ];
+        path = [
+          pkgs.wireguard-tools
+          config.networking.firewall.package   # iptables or nftables
+          config.networking.resolvconf.package # openresolv or systemd
+        ];
 
         serviceConfig = {
           Type = "oneshot";
@@ -281,7 +285,7 @@ let
         };
 
         script = ''
-          ${optionalString (!config.boot.isContainer) "modprobe wireguard"}
+          ${optionalString (!config.boot.isContainer) "${pkgs.kmod}/bin/modprobe wireguard"}
           ${optionalString (values.configFile != null) ''
             cp ${values.configFile} ${configPath}
           ''}
diff --git a/nixos/modules/services/networking/wgautomesh.nix b/nixos/modules/services/networking/wgautomesh.nix
new file mode 100644
index 00000000000..93227a9b625
--- /dev/null
+++ b/nixos/modules/services/networking/wgautomesh.nix
@@ -0,0 +1,161 @@
+{ lib, config, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.wgautomesh;
+  settingsFormat = pkgs.formats.toml { };
+  configFile =
+    # Have to remove nulls manually as TOML generator will not just skip key
+    # if value is null
+    settingsFormat.generate "wgautomesh-config.toml"
+      (filterAttrs (k: v: v != null)
+        (mapAttrs
+          (k: v:
+            if k == "peers"
+            then map (e: filterAttrs (k: v: v != null) e) v
+            else v)
+          cfg.settings));
+  runtimeConfigFile =
+    if cfg.enableGossipEncryption
+    then "/run/wgautomesh/wgautomesh.toml"
+    else configFile;
+in
+{
+  options.services.wgautomesh = {
+    enable = mkEnableOption (mdDoc "the wgautomesh daemon");
+    logLevel = mkOption {
+      type = types.enum [ "trace" "debug" "info" "warn" "error" ];
+      default = "info";
+      description = mdDoc "wgautomesh log level.";
+    };
+    enableGossipEncryption = mkOption {
+      type = types.bool;
+      default = true;
+      description = mdDoc "Enable encryption of gossip traffic.";
+    };
+    gossipSecretFile = mkOption {
+      type = types.path;
+      description = mdDoc ''
+        File containing the shared secret key to use for gossip encryption.
+        Required if `enableGossipEncryption` is set.
+      '';
+    };
+    enablePersistence = mkOption {
+      type = types.bool;
+      default = true;
+      description = mdDoc "Enable persistence of Wireguard peer info between restarts.";
+    };
+    openFirewall = mkOption {
+      type = types.bool;
+      default = true;
+      description = mdDoc "Automatically open gossip port in firewall (recommended).";
+    };
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = settingsFormat.type;
+        options = {
+
+          interface = mkOption {
+            type = types.str;
+            description = mdDoc ''
+              Wireguard interface to manage (it is NOT created by wgautomesh, you
+              should use another NixOS option to create it such as
+              `networking.wireguard.interfaces.wg0 = {...};`).
+            '';
+            example = "wg0";
+          };
+          gossip_port = mkOption {
+            type = types.port;
+            description = mdDoc ''
+              wgautomesh gossip port, this MUST be the same number on all nodes in
+              the wgautomesh network.
+            '';
+            default = 1666;
+          };
+          lan_discovery = mkOption {
+            type = types.bool;
+            default = true;
+            description = mdDoc "Enable discovery of peers on the same LAN using UDP broadcast.";
+          };
+          upnp_forward_external_port = mkOption {
+            type = types.nullOr types.port;
+            default = null;
+            description = mdDoc ''
+              Public port number to try to redirect to this machine's Wireguard
+              daemon using UPnP IGD.
+            '';
+          };
+          peers = mkOption {
+            type = types.listOf (types.submodule {
+              options = {
+                pubkey = mkOption {
+                  type = types.str;
+                  description = mdDoc "Wireguard public key of this peer.";
+                };
+                address = mkOption {
+                  type = types.str;
+                  description = mdDoc ''
+                    Wireguard address of this peer (a single IP address, multliple
+                    addresses or address ranges are not supported).
+                  '';
+                  example = "10.0.0.42";
+                };
+                endpoint = mkOption {
+                  type = types.nullOr types.str;
+                  description = mdDoc ''
+                    Bootstrap endpoint for connecting to this Wireguard peer if no
+                    other address is known or none are working.
+                  '';
+                  default = null;
+                  example = "wgnode.mydomain.example:51820";
+                };
+              };
+            });
+            default = [ ];
+            description = mdDoc "wgautomesh peer list.";
+          };
+        };
+
+      };
+      default = { };
+      description = mdDoc "Configuration for wgautomesh.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.wgautomesh.settings = {
+      gossip_secret_file = mkIf cfg.enableGossipEncryption "$CREDENTIALS_DIRECTORY/gossip_secret";
+      persist_file = mkIf cfg.enablePersistence "/var/lib/wgautomesh/state";
+    };
+
+    systemd.services.wgautomesh = {
+      path = [ pkgs.wireguard-tools ];
+      environment = { RUST_LOG = "wgautomesh=${cfg.logLevel}"; };
+      description = "wgautomesh";
+      serviceConfig = {
+        Type = "simple";
+
+        ExecStart = "${getExe pkgs.wgautomesh} ${runtimeConfigFile}";
+        Restart = "always";
+        RestartSec = "30";
+        LoadCredential = mkIf cfg.enableGossipEncryption [ "gossip_secret:${cfg.gossipSecretFile}" ];
+
+        ExecStartPre = mkIf cfg.enableGossipEncryption [
+          ''${pkgs.envsubst}/bin/envsubst \
+              -i ${configFile} \
+              -o ${runtimeConfigFile}''
+        ];
+
+        DynamicUser = true;
+        StateDirectory = "wgautomesh";
+        StateDirectoryMode = "0700";
+        RuntimeDirectory = "wgautomesh";
+        AmbientCapabilities = "CAP_NET_ADMIN";
+        CapabilityBoundingSet = "CAP_NET_ADMIN";
+      };
+      wantedBy = [ "multi-user.target" ];
+    };
+    networking.firewall.allowedUDPPorts =
+      mkIf cfg.openFirewall [ cfg.settings.gossip_port ];
+  };
+}
+
diff --git a/nixos/modules/services/networking/wireguard.nix b/nixos/modules/services/networking/wireguard.nix
index 1d6556f626b..8b025228cc1 100644
--- a/nixos/modules/services/networking/wireguard.nix
+++ b/nixos/modules/services/networking/wireguard.nix
@@ -176,7 +176,7 @@ let
 
       publicKey = mkOption {
         example = "xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=";
-        type = types.str;
+        type = types.singleLineStr;
         description = lib.mdDoc "The base64 public key of the peer.";
       };
 
@@ -461,7 +461,7 @@ let
 
           ${ipPreMove} link add dev "${name}" type wireguard
           ${optionalString (values.interfaceNamespace != null && values.interfaceNamespace != values.socketNamespace) ''${ipPreMove} link set "${name}" netns "${ns}"''}
-          ${optionalString (values.mtu != null) ''${ipPreMove} link set "${name}" mtu ${toString values.mtu}''}
+          ${optionalString (values.mtu != null) ''${ipPostMove} link set "${name}" mtu ${toString values.mtu}''}
 
           ${concatMapStringsSep "\n" (ip:
             ''${ipPostMove} address add "${ip}" dev "${name}"''
diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix
index 119575bdddb..0595e9e6df2 100644
--- a/nixos/modules/services/networking/wpa_supplicant.nix
+++ b/nixos/modules/services/networking/wpa_supplicant.nix
@@ -121,11 +121,15 @@ let
         ''}
 
         # substitute environment variables
-        ${pkgs.gawk}/bin/awk '{
-          for(varname in ENVIRON)
-            gsub("@"varname"@", ENVIRON[varname])
-          print
-        }' "${configFile}" > "${finalConfig}"
+        if [ -f "${configFile}" ]; then
+          ${pkgs.gawk}/bin/awk '{
+            for(varname in ENVIRON)
+              gsub("@"varname"@", ENVIRON[varname])
+            print
+          }' "${configFile}" > "${finalConfig}"
+        else
+          touch "${finalConfig}"
+        fi
 
         iface_args="-s ${optionalString cfg.dbusControlled "-u"} -D${cfg.driver} ${configStr}"
 
diff --git a/nixos/modules/services/networking/wstunnel.nix b/nixos/modules/services/networking/wstunnel.nix
new file mode 100644
index 00000000000..067d5df4872
--- /dev/null
+++ b/nixos/modules/services/networking/wstunnel.nix
@@ -0,0 +1,429 @@
+{ config, lib, options, pkgs, utils, ... }:
+with lib;
+let
+  cfg = config.services.wstunnel;
+  attrsToArgs = attrs: utils.escapeSystemdExecArgs (
+    mapAttrsToList
+    (name: value: if value == true then "--${name}" else "--${name}=${value}")
+    attrs
+  );
+  hostPortSubmodule = {
+    options = {
+      host = mkOption {
+        description = mdDoc "The hostname.";
+        type = types.str;
+      };
+      port = mkOption {
+        description = mdDoc "The port.";
+        type = types.port;
+      };
+    };
+  };
+  localRemoteSubmodule = {
+    options = {
+      local = mkOption {
+        description = mdDoc "Local address and port to listen on.";
+        type = types.submodule hostPortSubmodule;
+        example = {
+          host = "127.0.0.1";
+          port = 51820;
+        };
+      };
+      remote = mkOption {
+        description = mdDoc "Address and port on remote to forward traffic to.";
+        type = types.submodule hostPortSubmodule;
+        example = {
+          host = "127.0.0.1";
+          port = 51820;
+        };
+      };
+    };
+  };
+  hostPortToString = { host, port }: "${host}:${builtins.toString port}";
+  localRemoteToString = { local, remote }: utils.escapeSystemdExecArg "${hostPortToString local}:${hostPortToString remote}";
+  commonOptions = {
+    enable = mkOption {
+      description = mdDoc "Whether to enable this `wstunnel` instance.";
+      type = types.bool;
+      default = true;
+    };
+
+    package = mkPackageOptionMD pkgs "wstunnel" {};
+
+    autoStart = mkOption {
+      description = mdDoc "Whether this tunnel server should be started automatically.";
+      type = types.bool;
+      default = true;
+    };
+
+    extraArgs = mkOption {
+      description = mdDoc "Extra command line arguments to pass to `wstunnel`. Attributes of the form `argName = true;` will be translated to `--argName`, and `argName = \"value\"` to `--argName=value`.";
+      type = with types; attrsOf (either str bool);
+      default = {};
+      example = {
+        "someNewOption" = true;
+        "someNewOptionWithValue" = "someValue";
+      };
+    };
+
+    verboseLogging = mkOption {
+      description = mdDoc "Enable verbose logging.";
+      type = types.bool;
+      default = false;
+    };
+
+    environmentFile = mkOption {
+      description = mdDoc "Environment file to be passed to the systemd service. Useful for passing secrets to the service to prevent them from being world-readable in the Nix store. Note however that the secrets are passed to `wstunnel` through the command line, which makes them locally readable for all users of the system at runtime.";
+      type = types.nullOr types.path;
+      default = null;
+      example = "/var/lib/secrets/wstunnelSecrets";
+    };
+  };
+
+  serverSubmodule = { config, ...}: {
+    options = commonOptions // {
+      listen = mkOption {
+        description = mdDoc "Address and port to listen on. Setting the port to a value below 1024 will also give the process the required `CAP_NET_BIND_SERVICE` capability.";
+        type = types.submodule hostPortSubmodule;
+        default = {
+          address = "0.0.0.0";
+          port = if config.enableHTTPS then 443 else 80;
+        };
+        defaultText = literalExpression ''
+          {
+            address = "0.0.0.0";
+            port = if enableHTTPS then 443 else 80;
+          }
+        '';
+      };
+
+      restrictTo = mkOption {
+        description = mdDoc "Accepted traffic will be forwarded only to this service. Set to `null` to allow forwarding to arbitrary addresses.";
+        type = types.nullOr (types.submodule hostPortSubmodule);
+        example = {
+          host = "127.0.0.1";
+          port = 51820;
+        };
+      };
+
+      enableHTTPS = mkOption {
+        description = mdDoc "Use HTTPS for the tunnel server.";
+        type = types.bool;
+        default = true;
+      };
+
+      tlsCertificate = mkOption {
+        description = mdDoc "TLS certificate to use instead of the hardcoded one in case of HTTPS connections. Use together with `tlsKey`.";
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/secrets/cert.pem";
+      };
+
+      tlsKey = mkOption {
+        description = mdDoc "TLS key to use instead of the hardcoded on in case of HTTPS connections. Use together with `tlsCertificate`.";
+        type = types.nullOr types.path;
+        default = null;
+        example = "/var/lib/secrets/key.pem";
+      };
+
+      useACMEHost = mkOption {
+        description = mdDoc "Use a certificate generated by the NixOS ACME module for the given host. Note that this will not generate a new certificate - you will need to do so with `security.acme.certs`.";
+        type = types.nullOr types.str;
+        default = null;
+        example = "example.com";
+      };
+    };
+  };
+  clientSubmodule = { config, ... }: {
+    options = commonOptions // {
+      connectTo = mkOption {
+        description = mdDoc "Server address and port to connect to.";
+        type = types.submodule hostPortSubmodule;
+        example = {
+          host = "example.com";
+        };
+      };
+
+      enableHTTPS = mkOption {
+        description = mdDoc "Enable HTTPS when connecting to the server.";
+        type = types.bool;
+        default = true;
+      };
+
+      localToRemote = mkOption {
+        description = mdDoc "Local hosts and ports to listen on, plus the hosts and ports on remote to forward traffic to. Setting a local port to a value less than 1024 will additionally give the process the required CAP_NET_BIND_SERVICE capability.";
+        type = types.listOf (types.submodule localRemoteSubmodule);
+        default = [];
+        example = [ {
+          local = {
+            host = "127.0.0.1";
+            port = 8080;
+          };
+          remote = {
+            host = "127.0.0.1";
+            port = 8080;
+          };
+        } ];
+      };
+
+      dynamicToRemote = mkOption {
+        description = mdDoc "Host and port for the SOCKS5 proxy to dynamically forward traffic to. Leave this at `null` to disable the SOCKS5 proxy. Setting the port to a value less than 1024 will additionally give the service the required CAP_NET_BIND_SERVICE capability.";
+        type = types.nullOr (types.submodule hostPortSubmodule);
+        default = null;
+        example = {
+          host = "127.0.0.1";
+          port = 1080;
+        };
+      };
+
+      udp = mkOption {
+        description = mdDoc "Whether to forward UDP instead of TCP traffic.";
+        type = types.bool;
+        default = false;
+      };
+
+      udpTimeout = mkOption {
+        description = mdDoc "When using UDP forwarding, timeout in seconds after which the tunnel connection is closed. `-1` means no timeout.";
+        type = types.int;
+        default = 30;
+      };
+
+      httpProxy = mkOption {
+        description = mdDoc ''
+          Proxy to use to connect to the wstunnel server (`USER:PASS@HOST:PORT`).
+
+          ::: {.warning}
+          Passwords specified here will be world-readable in the Nix store! To pass a password to the service, point the `environmentFile` option to a file containing `PROXY_PASSWORD=<your-password-here>` and set this option to `<user>:$PROXY_PASSWORD@<host>:<port>`. Note however that this will also locally leak the passwords at runtime via e.g. /proc/<pid>/cmdline.
+
+          :::
+        '';
+        type = types.nullOr types.str;
+        default = null;
+      };
+
+      soMark = mkOption {
+        description = mdDoc "Mark network packets with the SO_MARK sockoption with the specified value. Setting this option will also enable the required `CAP_NET_ADMIN` capability for the systemd service.";
+        type = types.nullOr types.int;
+        default = null;
+      };
+
+      upgradePathPrefix = mkOption {
+        description = mdDoc "Use a specific HTTP path prefix that will show up in the upgrade request to the `wstunnel` server. Useful when running `wstunnel` behind a reverse proxy.";
+        type = types.nullOr types.str;
+        default = null;
+        example = "wstunnel";
+      };
+
+      hostHeader = mkOption {
+        description = mdDoc "Use this as the HTTP host header instead of the real hostname. Useful for circumventing hostname-based firewalls.";
+        type = types.nullOr types.str;
+        default = null;
+      };
+
+      tlsSNI = mkOption {
+        description = mdDoc "Use this as the SNI while connecting via TLS. Useful for circumventing hostname-based firewalls.";
+        type = types.nullOr types.str;
+        default = null;
+      };
+
+      tlsVerifyCertificate = mkOption {
+        description = mdDoc "Whether to verify the TLS certificate of the server. It might be useful to set this to `false` when working with the `tlsSNI` option.";
+        type = types.bool;
+        default = true;
+      };
+
+      # The original argument name `websocketPingFrequency` is a misnomer, as the frequency is the inverse of the interval.
+      websocketPingInterval = mkOption {
+        description = mdDoc "Do a heartbeat ping every N seconds to keep up the websocket connection.";
+        type = types.nullOr types.ints.unsigned;
+        default = null;
+      };
+
+      upgradeCredentials = mkOption {
+        description = mdDoc ''
+          Use these credentials to authenticate during the HTTP upgrade request (Basic authorization type, `USER:[PASS]`).
+
+          ::: {.warning}
+          Passwords specified here will be world-readable in the Nix store! To pass a password to the service, point the `environmentFile` option to a file containing `HTTP_PASSWORD=<your-password-here>` and set this option to `<user>:$HTTP_PASSWORD`. Note however that this will also locally leak the passwords at runtime via e.g. /proc/<pid>/cmdline.
+          :::
+        '';
+        type = types.nullOr types.str;
+        default = null;
+      };
+
+      customHeaders = mkOption {
+        description = mdDoc "Custom HTTP headers to send during the upgrade request.";
+        type = types.attrsOf types.str;
+        default = {};
+        example = {
+          "X-Some-Header" = "some-value";
+        };
+      };
+    };
+  };
+  generateServerUnit = name: serverCfg: {
+    name = "wstunnel-server-${name}";
+    value = {
+      description = "wstunnel server - ${name}";
+      requires = [ "network.target" "network-online.target" ];
+      after = [ "network.target" "network-online.target" ];
+      wantedBy = optional serverCfg.autoStart "multi-user.target";
+
+      serviceConfig = let
+        certConfig = config.security.acme.certs."${serverCfg.useACMEHost}";
+      in {
+        Type = "simple";
+        ExecStart = with serverCfg; let
+          resolvedTlsCertificate = if useACMEHost != null
+            then "${certConfig.directory}/fullchain.pem"
+            else tlsCertificate;
+          resolvedTlsKey = if useACMEHost != null
+            then "${certConfig.directory}/key.pem"
+            else tlsKey;
+        in ''
+          ${package}/bin/wstunnel \
+            --server \
+            ${optionalString (restrictTo != null)     "--restrictTo=${utils.escapeSystemdExecArg (hostPortToString restrictTo)}"} \
+            ${optionalString (resolvedTlsCertificate != null) "--tlsCertificate=${utils.escapeSystemdExecArg resolvedTlsCertificate}"} \
+            ${optionalString (resolvedTlsKey != null)         "--tlsKey=${utils.escapeSystemdExecArg resolvedTlsKey}"} \
+            ${optionalString verboseLogging "--verbose"} \
+            ${attrsToArgs extraArgs} \
+            ${utils.escapeSystemdExecArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString listen}"}
+        '';
+        EnvironmentFile = optional (serverCfg.environmentFile != null) serverCfg.environmentFile;
+        DynamicUser = true;
+        SupplementaryGroups = optional (serverCfg.useACMEHost != null) certConfig.group;
+        PrivateTmp = true;
+        AmbientCapabilities = optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ];
+        NoNewPrivileges = true;
+        RestrictNamespaces = "uts ipc pid user cgroup";
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        PrivateDevices = true;
+        RestrictSUIDSGID = true;
+
+      };
+    };
+  };
+  generateClientUnit = name: clientCfg: {
+    name = "wstunnel-client-${name}";
+    value = {
+      description = "wstunnel client - ${name}";
+      requires = [ "network.target" "network-online.target" ];
+      after = [ "network.target" "network-online.target" ];
+      wantedBy = optional clientCfg.autoStart "multi-user.target";
+
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = with clientCfg; ''
+          ${package}/bin/wstunnel \
+            ${concatStringsSep " " (builtins.map (x:          "--localToRemote=${localRemoteToString x}") localToRemote)} \
+            ${concatStringsSep " " (mapAttrsToList (n: v:     "--customHeaders=\"${n}: ${v}\"") customHeaders)} \
+            ${optionalString (dynamicToRemote != null)        "--dynamicToRemote=${utils.escapeSystemdExecArg (hostPortToString dynamicToRemote)}"} \
+            ${optionalString udp                              "--udp"} \
+            ${optionalString (httpProxy != null)              "--httpProxy=${httpProxy}"} \
+            ${optionalString (soMark != null)                 "--soMark=${toString soMark}"} \
+            ${optionalString (upgradePathPrefix != null)      "--upgradePathPrefix=${upgradePathPrefix}"} \
+            ${optionalString (hostHeader != null)             "--hostHeader=${hostHeader}"} \
+            ${optionalString (tlsSNI != null)                 "--tlsSNI=${tlsSNI}"} \
+            ${optionalString tlsVerifyCertificate             "--tlsVerifyCertificate"} \
+            ${optionalString (websocketPingInterval != null)  "--websocketPingFrequency=${toString websocketPingInterval}"} \
+            ${optionalString (upgradeCredentials != null)     "--upgradeCredentials=${upgradeCredentials}"} \
+            --udpTimeoutSec=${toString udpTimeout} \
+            ${optionalString verboseLogging "--verbose"} \
+            ${attrsToArgs extraArgs} \
+            ${utils.escapeSystemdExecArg "${if enableHTTPS then "wss" else "ws"}://${hostPortToString connectTo}"}
+        '';
+        EnvironmentFile = optional (clientCfg.environmentFile != null) clientCfg.environmentFile;
+        DynamicUser = true;
+        PrivateTmp = true;
+        AmbientCapabilities = (optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optionals ((clientCfg.dynamicToRemote.port or 1024) < 1024 || (any (x: x.local.port < 1024) clientCfg.localToRemote)) [ "CAP_NET_BIND_SERVICE" ]);
+        NoNewPrivileges = true;
+        RestrictNamespaces = "uts ipc pid user cgroup";
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectControlGroups = true;
+        PrivateDevices = true;
+        RestrictSUIDSGID = true;
+      };
+    };
+  };
+in {
+  options.services.wstunnel = {
+    enable = mkEnableOption (mdDoc "wstunnel");
+
+    servers = mkOption {
+      description = mdDoc "`wstunnel` servers to set up.";
+      type = types.attrsOf (types.submodule serverSubmodule);
+      default = {};
+      example = {
+        "wg-tunnel" = {
+          listen.port = 8080;
+          enableHTTPS = true;
+          tlsCertificate = "/var/lib/secrets/fullchain.pem";
+          tlsKey = "/var/lib/secrets/key.pem";
+          restrictTo = {
+            host = "127.0.0.1";
+            port = 51820;
+          };
+        };
+      };
+    };
+
+    clients = mkOption {
+      description = mdDoc "`wstunnel` clients to set up.";
+      type = types.attrsOf (types.submodule clientSubmodule);
+      default = {};
+      example = {
+        "wg-tunnel" = {
+          connectTo = {
+            host = "example.com";
+            port = 8080;
+          };
+          enableHTTPS = true;
+          localToRemote = {
+            local = {
+              host = "127.0.0.1";
+              port = 51820;
+            };
+            remote = {
+              host = "127.0.0.1";
+              port = 51820;
+            };
+          };
+          udp = true;
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services = (mapAttrs' generateServerUnit (filterAttrs (n: v: v.enable) cfg.servers)) // (mapAttrs' generateClientUnit (filterAttrs (n: v: v.enable) cfg.clients));
+
+    assertions = (mapAttrsToList (name: serverCfg: {
+      assertion = !(serverCfg.useACMEHost != null && (serverCfg.tlsCertificate != null || serverCfg.tlsKey != null));
+      message = ''
+        Options services.wstunnel.servers."${name}".useACMEHost and services.wstunnel.servers."${name}".{tlsCertificate, tlsKey} are mutually exclusive.
+      '';
+    }) cfg.servers) ++
+    (mapAttrsToList (name: serverCfg: {
+      assertion = !((serverCfg.tlsCertificate != null || serverCfg.tlsKey != null) && !(serverCfg.tlsCertificate != null && serverCfg.tlsKey != null));
+      message = ''
+        services.wstunnel.servers."${name}".tlsCertificate and services.wstunnel.servers."${name}".tlsKey need to be set together.
+      '';
+    }) cfg.servers) ++
+    (mapAttrsToList (name: clientCfg: {
+      assertion = !(clientCfg.localToRemote == [] && clientCfg.dynamicToRemote == null);
+      message = ''
+        Either one of services.wstunnel.clients."${name}".localToRemote or services.wstunnel.clients."${name}".dynamicToRemote must be set.
+      '';
+    }) cfg.clients);
+  };
+
+  meta.maintainers = with maintainers; [ alyaeanyx ];
+}
diff --git a/nixos/modules/services/networking/xinetd.nix b/nixos/modules/services/networking/xinetd.nix
index b9120f37ba2..fb3de7077e3 100644
--- a/nixos/modules/services/networking/xinetd.nix
+++ b/nixos/modules/services/networking/xinetd.nix
@@ -27,7 +27,7 @@ let
         ${optionalString srv.unlisted "type        = UNLISTED"}
         ${optionalString (srv.flags != "") "flags = ${srv.flags}"}
         socket_type = ${if srv.protocol == "udp" then "dgram" else "stream"}
-        ${if srv.port != 0 then "port        = ${toString srv.port}" else ""}
+        ${optionalString (srv.port != 0) "port        = ${toString srv.port}"}
         wait        = ${if srv.protocol == "udp" then "yes" else "no"}
         user        = ${srv.user}
         server      = ${srv.server}
diff --git a/nixos/modules/services/networking/yggdrasil.xml b/nixos/modules/services/networking/yggdrasil.md
index a7b8c469529..bbaea5bc74a 100644
--- a/nixos/modules/services/networking/yggdrasil.xml
+++ b/nixos/modules/services/networking/yggdrasil.md
@@ -1,25 +1,18 @@
-<?xml version="1.0"?>
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xi="http://www.w3.org/2001/XInclude" version="5.0" xml:id="module-services-networking-yggdrasil">
-  <title>Yggdrasil</title>
-  <para>
-    <emphasis>Source:</emphasis>
-    <filename>modules/services/networking/yggdrasil/default.nix</filename>
-  </para>
-  <para>
-    <emphasis>Upstream documentation:</emphasis>
-    <link xlink:href="https://yggdrasil-network.github.io/"/>
-  </para>
-  <para>
+# Yggdrasil {#module-services-networking-yggdrasil}
+
+*Source:* {file}`modules/services/networking/yggdrasil/default.nix`
+
+*Upstream documentation:* <https://yggdrasil-network.github.io/>
+
 Yggdrasil is an early-stage implementation of a fully end-to-end encrypted,
 self-arranging IPv6 network.
-</para>
-  <section xml:id="module-services-networking-yggdrasil-configuration">
-    <title>Configuration</title>
-    <section xml:id="module-services-networking-yggdrasil-configuration-simple">
-      <title>Simple ephemeral node</title>
-      <para>
+
+## Configuration {#module-services-networking-yggdrasil-configuration}
+
+### Simple ephemeral node {#module-services-networking-yggdrasil-configuration-simple}
+
 An annotated example of a simple configuration:
-<programlisting>
+```
 {
   services.yggdrasil = {
     enable = true;
@@ -41,14 +34,12 @@ An annotated example of a simple configuration:
     };
   };
 }
-</programlisting>
-   </para>
-    </section>
-    <section xml:id="module-services-networking-yggdrasil-configuration-prefix">
-      <title>Persistent node with prefix</title>
-      <para>
+```
+
+### Persistent node with prefix {#module-services-networking-yggdrasil-configuration-prefix}
+
 A node with a fixed address that announces a prefix:
-<programlisting>
+```
 let
   address = "210:5217:69c0:9afc:1b95:b9f:8718:c3d2";
   prefix = "310:5217:69c0:9afc";
@@ -93,15 +84,13 @@ in {
     '';
   };
 }
-</programlisting>
-  </para>
-    </section>
-    <section xml:id="module-services-networking-yggdrasil-configuration-container">
-      <title>Yggdrasil attached Container</title>
-      <para>
+```
+
+### Yggdrasil attached Container {#module-services-networking-yggdrasil-configuration-container}
+
 A NixOS container attached to the Yggdrasil network via a node running on the
 host:
-        <programlisting>
+```
 let
   yggPrefix64 = "310:5217:69c0:9afc";
     # Again, taken from the output of "yggdrasilctl getself".
@@ -112,10 +101,10 @@ in
 
   networking = {
     bridges.br0.interfaces = [ ];
-    # A bridge only to containers&#x2026;
+    # A bridge only to containers…
 
     interfaces.br0 = {
-      # &#x2026; configured with a prefix address.
+      # … configured with a prefix address.
       ipv6.addresses = [{
         address = "${yggPrefix64}::1";
         prefixLength = 64;
@@ -149,8 +138,4 @@ in
   };
 
 }
-</programlisting>
-      </para>
-    </section>
-  </section>
-</chapter>
+```
diff --git a/nixos/modules/services/networking/yggdrasil.nix b/nixos/modules/services/networking/yggdrasil.nix
index 3d5cbdd2dc3..55a6002d61a 100644
--- a/nixos/modules/services/networking/yggdrasil.nix
+++ b/nixos/modules/services/networking/yggdrasil.nix
@@ -8,7 +8,8 @@ let
   configFileProvided = cfg.configFile != null;
 
   format = pkgs.formats.json { };
-in {
+in
+{
   imports = [
     (mkRenamedOptionModule
       [ "services" "yggdrasil" "config" ]
@@ -21,7 +22,7 @@ in {
 
       settings = mkOption {
         type = format.type;
-        default = {};
+        default = { };
         example = {
           Peers = [
             "tcp://aa.bb.cc.dd:eeeee"
@@ -45,7 +46,7 @@ in {
 
           If no keys are specified then ephemeral keys are generated
           and the Yggdrasil interface will have a random IPv6 address
-          each time the service is started, this is the default.
+          each time the service is started. This is the default.
 
           If both {option}`configFile` and {option}`settings`
           are supplied, they will be combined, with values from
@@ -61,8 +62,13 @@ in {
         default = null;
         example = "/run/keys/yggdrasil.conf";
         description = lib.mdDoc ''
-          A file which contains JSON configuration for yggdrasil.
-          See the {option}`settings` option for more information.
+          A file which contains JSON or HJSON configuration for yggdrasil. See
+          the {option}`settings` option for more information.
+
+          Note: This file must not be larger than 1 MB because it is passed to
+          the yggdrasil process via systemd‘s LoadCredential mechanism. For
+          details, see <https://systemd.io/CREDENTIALS/> and `man 5
+          systemd.exec`.
         '';
       };
 
@@ -77,20 +83,20 @@ in {
         type = bool;
         default = false;
         description = lib.mdDoc ''
-          Whether to open the UDP port used for multicast peer
-          discovery. The NixOS firewall blocks link-local
-          communication, so in order to make local peering work you
-          will also need to set `LinkLocalTCPPort` in your
-          yggdrasil configuration ({option}`settings` or
-          {option}`configFile`) to a port number other than 0,
-          and then add that port to
-          {option}`networking.firewall.allowedTCPPorts`.
+          Whether to open the UDP port used for multicast peer discovery. The
+          NixOS firewall blocks link-local communication, so in order to make
+          incoming local peering work you will also need to configure
+          `MulticastInterfaces` in your Yggdrasil configuration
+          ({option}`settings` or {option}`configFile`). You will then have to
+          add the ports that you configure there to your firewall configuration
+          ({option}`networking.firewall.allowedTCPPorts` or
+          {option}`networking.firewall.interfaces.<name>.allowedTCPPorts`).
         '';
       };
 
       denyDhcpcdInterfaces = mkOption {
         type = listOf str;
-        default = [];
+        default = [ ];
         example = [ "tap*" ];
         description = lib.mdDoc ''
           Disable the DHCP client for any interface whose name matches
@@ -118,82 +124,104 @@ in {
     };
   };
 
-  config = mkIf cfg.enable (let binYggdrasil = cfg.package + "/bin/yggdrasil";
-  in {
-    assertions = [{
-      assertion = config.networking.enableIPv6;
-      message = "networking.enableIPv6 must be true for yggdrasil to work";
-    }];
-
-    system.activationScripts.yggdrasil = mkIf cfg.persistentKeys ''
-      if [ ! -e ${keysPath} ]
-      then
-        mkdir --mode=700 -p ${builtins.dirOf keysPath}
-        ${binYggdrasil} -genconf -json \
-          | ${pkgs.jq}/bin/jq \
-              'to_entries|map(select(.key|endswith("Key")))|from_entries' \
-          > ${keysPath}
-      fi
-    '';
-
-    systemd.services.yggdrasil = {
-      description = "Yggdrasil Network Service";
-      after = [ "network-pre.target" ];
-      wants = [ "network.target" ];
-      before = [ "network.target" ];
-      wantedBy = [ "multi-user.target" ];
-
-      preStart =
-        (if settingsProvided || configFileProvided || cfg.persistentKeys then
-          "echo "
-
-          + (lib.optionalString settingsProvided
-            "'${builtins.toJSON cfg.settings}'")
-          + (lib.optionalString configFileProvided "$(cat ${cfg.configFile})")
-          + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})")
-          + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf"
-        else
-          "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf";
-
-      serviceConfig = {
-        ExecStart =
-          "${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf";
-        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-        Restart = "always";
-
-        DynamicUser = true;
-        StateDirectory = "yggdrasil";
-        RuntimeDirectory = "yggdrasil";
-        RuntimeDirectoryMode = "0750";
-        BindReadOnlyPaths = lib.optional configFileProvided cfg.configFile
-          ++ lib.optional cfg.persistentKeys keysPath;
-        ReadWritePaths = "/run/yggdrasil";
-
-        AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
-        CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
-        MemoryDenyWriteExecute = true;
-        ProtectControlGroups = true;
-        ProtectHome = "tmpfs";
-        ProtectKernelModules = true;
-        ProtectKernelTunables = true;
-        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
-        RestrictNamespaces = true;
-        RestrictRealtime = true;
-        SystemCallArchitectures = "native";
-        SystemCallFilter = [ "@system-service" "~@privileged @keyring" ];
-      } // (if (cfg.group != null) then {
-        Group = cfg.group;
-      } else {});
-    };
+  config = mkIf cfg.enable (
+    let
+      binYggdrasil = "${cfg.package}/bin/yggdrasil";
+      binHjson = "${pkgs.hjson-go}/bin/hjson-cli";
+    in
+    {
+      assertions = [{
+        assertion = config.networking.enableIPv6;
+        message = "networking.enableIPv6 must be true for yggdrasil to work";
+      }];
+
+      system.activationScripts.yggdrasil = mkIf cfg.persistentKeys ''
+        if [ ! -e ${keysPath} ]
+        then
+          mkdir --mode=700 -p ${builtins.dirOf keysPath}
+          ${binYggdrasil} -genconf -json \
+            | ${pkgs.jq}/bin/jq \
+                'to_entries|map(select(.key|endswith("Key")))|from_entries' \
+            > ${keysPath}
+        fi
+      '';
+
+      systemd.services.yggdrasil = {
+        description = "Yggdrasil Network Service";
+        after = [ "network-pre.target" ];
+        wants = [ "network.target" ];
+        before = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        # This script first prepares the config file, then it starts Yggdrasil.
+        # The preparation could also be done in ExecStartPre/preStart but only
+        # systemd versions >= v252 support reading credentials in ExecStartPre. As
+        # of February 2023, systemd v252 is not yet in the stable branch of NixOS.
+        #
+        # This could be changed in the future once systemd version v252 has
+        # reached NixOS but it does not have to be. Config file preparation is
+        # fast enough, it does not need elevated privileges, and `set -euo
+        # pipefail` should make sure that the service is not started if the
+        # preparation fails. Therefore, it is not necessary to move the
+        # preparation to ExecStartPre.
+        script = ''
+          set -euo pipefail
+
+          # prepare config file
+          ${(if settingsProvided || configFileProvided || cfg.persistentKeys then
+            "echo "
+
+            + (lib.optionalString settingsProvided
+              "'${builtins.toJSON cfg.settings}'")
+            + (lib.optionalString configFileProvided
+              "$(${binHjson} -c \"$CREDENTIALS_DIRECTORY/yggdrasil.conf\")")
+            + (lib.optionalString cfg.persistentKeys "$(cat ${keysPath})")
+            + " | ${pkgs.jq}/bin/jq -s add | ${binYggdrasil} -normaliseconf -useconf"
+          else
+            "${binYggdrasil} -genconf") + " > /run/yggdrasil/yggdrasil.conf"}
+
+          # start yggdrasil
+          ${binYggdrasil} -useconffile /run/yggdrasil/yggdrasil.conf
+        '';
+
+        serviceConfig = {
+          ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+          Restart = "always";
+
+          DynamicUser = true;
+          StateDirectory = "yggdrasil";
+          RuntimeDirectory = "yggdrasil";
+          RuntimeDirectoryMode = "0750";
+          BindReadOnlyPaths = lib.optional cfg.persistentKeys keysPath;
+          LoadCredential =
+            mkIf configFileProvided "yggdrasil.conf:${cfg.configFile}";
+
+          AmbientCapabilities = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
+          CapabilityBoundingSet = "CAP_NET_ADMIN CAP_NET_BIND_SERVICE";
+          MemoryDenyWriteExecute = true;
+          ProtectControlGroups = true;
+          ProtectHome = "tmpfs";
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK";
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [ "@system-service" "~@privileged @keyring" ];
+        } // (if (cfg.group != null) then {
+          Group = cfg.group;
+        } else { });
+      };
 
-    networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
-    networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ];
+      networking.dhcpcd.denyInterfaces = cfg.denyDhcpcdInterfaces;
+      networking.firewall.allowedUDPPorts = mkIf cfg.openMulticastPort [ 9001 ];
 
-    # Make yggdrasilctl available on the command line.
-    environment.systemPackages = [ cfg.package ];
-  });
+      # Make yggdrasilctl available on the command line.
+      environment.systemPackages = [ cfg.package ];
+    }
+  );
   meta = {
-    doc = ./yggdrasil.xml;
+    doc = ./yggdrasil.md;
     maintainers = with lib.maintainers; [ gazally ehmry ];
   };
 }
diff --git a/nixos/modules/services/networking/znc/default.nix b/nixos/modules/services/networking/znc/default.nix
index 2befab373ba..d3ba4a52419 100644
--- a/nixos/modules/services/networking/znc/default.nix
+++ b/nixos/modules/services/networking/znc/default.nix
@@ -169,7 +169,7 @@ in
           gracefully be applied to this option.
 
           If you intend to update the configuration through this option, be sure
-          to enable {option}`services.znc.mutable`, otherwise none of the
+          to disable {option}`services.znc.mutable`, otherwise none of the
           changes here will be applied after the initial deploy.
         '';
       };
diff --git a/nixos/modules/services/printing/cups-pdf.nix b/nixos/modules/services/printing/cups-pdf.nix
new file mode 100644
index 00000000000..07f24367132
--- /dev/null
+++ b/nixos/modules/services/printing/cups-pdf.nix
@@ -0,0 +1,185 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  # cups calls its backends as user `lp` (which is good!),
+  # but cups-pdf wants to be called as `root`, so it can change ownership of files.
+  # We add a suid wrapper and a wrapper script to trick cups into calling the suid wrapper.
+  # Note that a symlink to the suid wrapper alone wouldn't suffice, cups would complain
+  # > File "/nix/store/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-cups-progs/lib/cups/backend/cups-pdf" has insecure permissions (0104554/uid=0/gid=20)
+
+  # wrapper script that redirects calls to the suid wrapper
+  cups-pdf-wrapper = pkgs.writeTextFile {
+    name = "${pkgs.cups-pdf-to-pdf.name}-wrapper.sh";
+    executable = true;
+    destination = "/lib/cups/backend/cups-pdf";
+    checkPhase = ''
+      ${pkgs.stdenv.shellDryRun} "$target"
+      ${lib.getExe pkgs.shellcheck} "$target"
+    '';
+    text = ''
+      #! ${pkgs.runtimeShell}
+      exec "${config.security.wrapperDir}/cups-pdf" "$@"
+    '';
+  };
+
+  # wrapped cups-pdf package that uses the suid wrapper
+  cups-pdf-wrapped = pkgs.buildEnv {
+    name = "${pkgs.cups-pdf-to-pdf.name}-wrapped";
+    # using the wrapper as first path ensures it is used
+    paths = [ cups-pdf-wrapper pkgs.cups-pdf-to-pdf ];
+    ignoreCollisions = true;
+  };
+
+  instanceSettings = name: {
+    freeformType = with lib.types; nullOr (oneOf [ int str path package ]);
+    # override defaults:
+    # inject instance name into paths,
+    # also avoid conflicts between user names and special dirs
+    options.Out = lib.mkOption {
+      type = with lib.types; nullOr singleLineStr;
+      default = "/var/spool/cups-pdf-${name}/users/\${USER}";
+      defaultText = "/var/spool/cups-pdf-{instance-name}/users/\${USER}";
+      example = "\${HOME}/cups-pdf";
+      description = lib.mdDoc ''
+        output directory;
+        `''${HOME}` will be expanded to the user's home directory,
+        `''${USER}` will be expanded to the user name.
+      '';
+    };
+    options.AnonDirName = lib.mkOption {
+      type = with lib.types; nullOr singleLineStr;
+      default = "/var/spool/cups-pdf-${name}/anonymous";
+      defaultText = "/var/spool/cups-pdf-{instance-name}/anonymous";
+      example = "/var/lib/cups-pdf";
+      description = lib.mdDoc "path for anonymously created PDF files";
+    };
+    options.Spool = lib.mkOption {
+      type = with lib.types; nullOr singleLineStr;
+      default = "/var/spool/cups-pdf-${name}/spool";
+      defaultText = "/var/spool/cups-pdf-{instance-name}/spool";
+      example = "/var/lib/cups-pdf";
+      description = lib.mdDoc "spool directory";
+    };
+    options.Anonuser = lib.mkOption {
+      type = lib.types.singleLineStr;
+      default = "root";
+      description = lib.mdDoc ''
+        User for anonymous PDF creation.
+        An empty string disables this feature.
+      '';
+    };
+    options.GhostScript = lib.mkOption {
+      type = with lib.types; nullOr path;
+      default = lib.getExe pkgs.ghostscript;
+      defaultText = lib.literalExpression "lib.getExe pkgs.ghostscript";
+      example = lib.literalExpression ''''${pkgs.ghostscript}/bin/ps2pdf'';
+      description = lib.mdDoc "location of GhostScript binary";
+    };
+  };
+
+  instanceConfig = { name, config, ... }: {
+    options = {
+      enable = (lib.mkEnableOption (lib.mdDoc "this cups-pdf instance")) // { default = true; };
+      installPrinter = (lib.mkEnableOption (lib.mdDoc ''
+        a CUPS printer queue for this instance.
+        The queue will be named after the instance and will use the {file}`CUPS-PDF_opt.ppd` ppd file.
+        If this is disabled, you need to add the queue yourself to use the instance
+      '')) // { default = true; };
+      confFileText = lib.mkOption {
+        type = lib.types.lines;
+        description = lib.mdDoc ''
+          This will contain the contents of {file}`cups-pdf.conf` for this instance, derived from {option}`settings`.
+          You can use this option to append text to the file.
+        '';
+      };
+      settings = lib.mkOption {
+        type = lib.types.submodule (instanceSettings name);
+        default = {};
+        example = {
+          Out = "\${HOME}/cups-pdf";
+          UserUMask = "0033";
+        };
+        description = lib.mdDoc ''
+          Settings for a cups-pdf instance, see the descriptions in the template config file in the cups-pdf package.
+          The key value pairs declared here will be translated into proper key value pairs for {file}`cups-pdf.conf`.
+          Setting a value to `null` disables the option and removes it from the file.
+        '';
+      };
+    };
+    config.confFileText = lib.pipe config.settings [
+      (lib.filterAttrs (key: value: value != null))
+      (lib.mapAttrs (key: builtins.toString))
+      (lib.mapAttrsToList (key: value: "${key} ${value}\n"))
+      lib.concatStrings
+    ];
+  };
+
+  cupsPdfCfg = config.services.printing.cups-pdf;
+
+  copyConfigFileCmds = lib.pipe cupsPdfCfg.instances [
+    (lib.filterAttrs (name: lib.getAttr "enable"))
+    (lib.mapAttrs (name: lib.getAttr "confFileText"))
+    (lib.mapAttrs (name: pkgs.writeText "cups-pdf-${name}.conf"))
+    (lib.mapAttrsToList (name: confFile: "ln --symbolic --no-target-directory ${confFile} /var/lib/cups/cups-pdf-${name}.conf\n"))
+    lib.concatStrings
+  ];
+
+  printerSettings = lib.pipe cupsPdfCfg.instances [
+    (lib.filterAttrs (name: lib.getAttr "enable"))
+    (lib.filterAttrs (name: lib.getAttr "installPrinter"))
+    (lib.mapAttrsToList (name: instance: (lib.mapAttrs (key: lib.mkDefault) {
+      inherit name;
+      model = "CUPS-PDF_opt.ppd";
+      deviceUri = "cups-pdf:/${name}";
+      description = "virtual printer for cups-pdf instance ${name}";
+      location = instance.settings.Out;
+    })))
+  ];
+
+in
+
+{
+
+  options.services.printing.cups-pdf = {
+    enable = lib.mkEnableOption (lib.mdDoc ''
+      the cups-pdf virtual pdf printer backend.
+      By default, this will install a single printer `pdf`.
+      but this can be changed/extended with {option}`services.printing.cups-pdf.instances`
+    '');
+    instances = lib.mkOption {
+      type = lib.types.attrsOf (lib.types.submodule instanceConfig);
+      default.pdf = {};
+      example.pdf.settings = {
+        Out = "\${HOME}/cups-pdf";
+        UserUMask = "0033";
+      };
+      description = lib.mdDoc ''
+        Permits to raise one or more cups-pdf instances.
+        Each instance is named by an attribute name, and the attribute's values control the instance' configuration.
+      '';
+    };
+  };
+
+  config = lib.mkIf cupsPdfCfg.enable {
+    services.printing.enable = true;
+    services.printing.drivers = [ cups-pdf-wrapped ];
+    hardware.printers.ensurePrinters = printerSettings;
+    # the cups module will install the default config file,
+    # but we don't need it and it would confuse cups-pdf
+    systemd.services.cups.preStart = lib.mkAfter ''
+      rm -f /var/lib/cups/cups-pdf.conf
+      ${copyConfigFileCmds}
+    '';
+    security.wrappers.cups-pdf = {
+      group = "lp";
+      owner = "root";
+      permissions = "+r,ug+x";
+      setuid = true;
+      source = "${pkgs.cups-pdf-to-pdf}/lib/cups/backend/cups-pdf";
+    };
+  };
+
+  meta.maintainers = [ lib.maintainers.yarny ];
+
+}
diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix
index ae59dcc226d..f6a23fb900f 100644
--- a/nixos/modules/services/printing/cupsd.nix
+++ b/nixos/modules/services/printing/cupsd.nix
@@ -317,6 +317,7 @@ in
     environment.etc.cups.source = "/var/lib/cups";
 
     services.dbus.packages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper;
+    services.udev.packages = cfg.drivers;
 
     # Allow asswordless printer admin for members of wheel group
     security.polkit.extraConfig = mkIf polkitEnabled ''
@@ -341,7 +342,7 @@ in
 
     systemd.sockets.cups = mkIf cfg.startWhenNeeded {
       wantedBy = [ "sockets.target" ];
-      listenStreams = [ "/run/cups/cups.sock" ]
+      listenStreams = [ "" "/run/cups/cups.sock" ]
         ++ map (x: replaceStrings ["localhost"] ["127.0.0.1"] (removePrefix "*:" x)) cfg.listenAddresses;
     };
 
@@ -395,10 +396,7 @@ in
             ''}
           '';
 
-          serviceConfig = {
-            PrivateTmp = true;
-            RuntimeDirectory = [ "cups" ];
-          };
+          serviceConfig.PrivateTmp = true;
       };
 
     systemd.services.cups-browsed = mkIf avahiEnabled
diff --git a/nixos/modules/services/search/meilisearch.md b/nixos/modules/services/search/meilisearch.md
index 98e7c542cb9..98af396117c 100644
--- a/nixos/modules/services/search/meilisearch.md
+++ b/nixos/modules/services/search/meilisearch.md
@@ -2,7 +2,7 @@
 
 Meilisearch is a lightweight, fast and powerful search engine. Think elastic search with a much smaller footprint.
 
-## Quickstart
+## Quickstart {#module-services-meilisearch-quickstart}
 
 the minimum to start meilisearch is
 
@@ -14,19 +14,19 @@ this will start the http server included with meilisearch on port 7700.
 
 test with `curl -X GET 'http://localhost:7700/health'`
 
-## Usage
+## Usage {#module-services-meilisearch-usage}
 
 you first need to add documents to an index before you can search for documents.
 
-### Add a documents to the `movies` index
+### Add a documents to the `movies` index {#module-services-meilisearch-quickstart-add}
 
 `curl -X POST 'http://127.0.0.1:7700/indexes/movies/documents' --data '[{"id": "123", "title": "Superman"}, {"id": 234, "title": "Batman"}]'`
 
-### Search documents in the `movies` index
+### Search documents in the `movies` index {#module-services-meilisearch-quickstart-search}
 
 `curl 'http://127.0.0.1:7700/indexes/movies/search' --data '{ "q": "botman" }'` (note the typo is intentional and there to demonstrate the typo tolerant capabilities)
 
-## Defaults
+## Defaults {#module-services-meilisearch-defaults}
 
 - The default nixos package doesn't come with the [dashboard](https://docs.meilisearch.com/learn/getting_started/quick_start.html#search), since the dashboard features makes some assets downloads at compile time.
 
@@ -34,6 +34,6 @@ you first need to add documents to an index before you can search for documents.
 
 - Default deployment is development mode. It doesn't require a secret master key. All routes are not protected and accessible.
 
-## Missing
+## Missing {#module-services-meilisearch-missing}
 
 - the snapshot feature is not yet configurable from the module, it's just a matter of adding the relevant environment variables.
diff --git a/nixos/modules/services/search/meilisearch.nix b/nixos/modules/services/search/meilisearch.nix
index 3983b1b2c92..7c9fa62ae95 100644
--- a/nixos/modules/services/search/meilisearch.nix
+++ b/nixos/modules/services/search/meilisearch.nix
@@ -9,9 +9,7 @@ in
 {
 
   meta.maintainers = with maintainers; [ Br1ght0ne happysalada ];
-  # Don't edit the docbook xml directly, edit the md and generate it:
-  # `pandoc meilisearch.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > meilisearch.xml`
-  meta.doc = ./meilisearch.xml;
+  meta.doc = ./meilisearch.md;
 
   ###### interface
 
@@ -117,7 +115,7 @@ in
         MEILI_HTTP_ADDR = "${cfg.listenAddress}:${toString cfg.listenPort}";
         MEILI_NO_ANALYTICS = toString cfg.noAnalytics;
         MEILI_ENV = cfg.environment;
-        MEILI_DUMPS_DIR = "/var/lib/meilisearch/dumps";
+        MEILI_DUMP_DIR = "/var/lib/meilisearch/dumps";
         MEILI_LOG_LEVEL = cfg.logLevel;
         MEILI_MAX_INDEX_SIZE = cfg.maxIndexSize;
       };
diff --git a/nixos/modules/services/search/meilisearch.xml b/nixos/modules/services/search/meilisearch.xml
deleted file mode 100644
index c1a73f358c2..00000000000
--- a/nixos/modules/services/search/meilisearch.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-meilisearch">
-  <title>Meilisearch</title>
-  <para>
-    Meilisearch is a lightweight, fast and powerful search engine. Think
-    elastic search with a much smaller footprint.
-  </para>
-  <section xml:id="quickstart">
-    <title>Quickstart</title>
-    <para>
-      the minimum to start meilisearch is
-    </para>
-    <programlisting language="bash">
-services.meilisearch.enable = true;
-</programlisting>
-    <para>
-      this will start the http server included with meilisearch on port
-      7700.
-    </para>
-    <para>
-      test with
-      <literal>curl -X GET 'http://localhost:7700/health'</literal>
-    </para>
-  </section>
-  <section xml:id="usage">
-    <title>Usage</title>
-    <para>
-      you first need to add documents to an index before you can search
-      for documents.
-    </para>
-    <section xml:id="add-a-documents-to-the-movies-index">
-      <title>Add a documents to the <literal>movies</literal>
-      index</title>
-      <para>
-        <literal>curl -X POST 'http://127.0.0.1:7700/indexes/movies/documents' --data '[{&quot;id&quot;: &quot;123&quot;, &quot;title&quot;: &quot;Superman&quot;}, {&quot;id&quot;: 234, &quot;title&quot;: &quot;Batman&quot;}]'</literal>
-      </para>
-    </section>
-    <section xml:id="search-documents-in-the-movies-index">
-      <title>Search documents in the <literal>movies</literal>
-      index</title>
-      <para>
-        <literal>curl 'http://127.0.0.1:7700/indexes/movies/search' --data '{ &quot;q&quot;: &quot;botman&quot; }'</literal>
-        (note the typo is intentional and there to demonstrate the typo
-        tolerant capabilities)
-      </para>
-    </section>
-  </section>
-  <section xml:id="defaults">
-    <title>Defaults</title>
-    <itemizedlist>
-      <listitem>
-        <para>
-          The default nixos package doesn’t come with the
-          <link xlink:href="https://docs.meilisearch.com/learn/getting_started/quick_start.html#search">dashboard</link>,
-          since the dashboard features makes some assets downloads at
-          compile time.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Anonimized Analytics sent to meilisearch are disabled by
-          default.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          Default deployment is development mode. It doesn’t require a
-          secret master key. All routes are not protected and
-          accessible.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="missing">
-    <title>Missing</title>
-    <itemizedlist spacing="compact">
-      <listitem>
-        <para>
-          the snapshot feature is not yet configurable from the module,
-          it’s just a matter of adding the relevant environment
-          variables.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/search/opensearch.nix b/nixos/modules/services/search/opensearch.nix
new file mode 100644
index 00000000000..9a50e796313
--- /dev/null
+++ b/nixos/modules/services/search/opensearch.nix
@@ -0,0 +1,248 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.opensearch;
+
+  settingsFormat = pkgs.formats.yaml {};
+
+  configDir = cfg.dataDir + "/config";
+
+  usingDefaultDataDir = cfg.dataDir == "/var/lib/opensearch";
+  usingDefaultUserAndGroup = cfg.user == "opensearch" && cfg.group == "opensearch";
+
+  opensearchYml = settingsFormat.generate "opensearch.yml" cfg.settings;
+
+  loggingConfigFilename = "log4j2.properties";
+  loggingConfigFile = pkgs.writeTextFile {
+    name = loggingConfigFilename;
+    text = cfg.logging;
+  };
+in
+{
+
+  options.services.opensearch = {
+    enable = mkEnableOption (lib.mdDoc "OpenSearch");
+
+    package = lib.mkPackageOptionMD pkgs "OpenSearch" {
+      default = [ "opensearch" ];
+    };
+
+    settings = lib.mkOption {
+      type = lib.types.submodule {
+        freeformType = settingsFormat.type;
+
+        options."network.host" = lib.mkOption {
+          type = lib.types.str;
+          default = "127.0.0.1";
+          description = lib.mdDoc ''
+            Which port this service should listen on.
+          '';
+        };
+
+        options."cluster.name" = lib.mkOption {
+          type = lib.types.str;
+          default = "opensearch";
+          description = lib.mdDoc ''
+            The name of the cluster.
+          '';
+        };
+
+        options."discovery.type" = lib.mkOption {
+          type = lib.types.str;
+          default = "single-node";
+          description = lib.mdDoc ''
+            The type of discovery to use.
+          '';
+        };
+
+        options."http.port" = lib.mkOption {
+          type = lib.types.port;
+          default = 9200;
+          description = lib.mdDoc ''
+            The port to listen on for HTTP traffic.
+          '';
+        };
+
+        options."transport.port" = lib.mkOption {
+          type = lib.types.port;
+          default = 9300;
+          description = lib.mdDoc ''
+            The port to listen on for transport traffic.
+          '';
+        };
+      };
+
+      default = {};
+
+      description = lib.mdDoc ''
+        OpenSearch configuration.
+      '';
+    };
+
+    logging = lib.mkOption {
+      description = lib.mdDoc "opensearch logging configuration.";
+
+      default = ''
+        logger.action.name = org.opensearch.action
+        logger.action.level = info
+
+        appender.console.type = Console
+        appender.console.name = console
+        appender.console.layout.type = PatternLayout
+        appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n
+
+        rootLogger.level = info
+        rootLogger.appenderRef.console.ref = console
+      '';
+      type = types.str;
+    };
+
+    dataDir = lib.mkOption {
+      type = lib.types.path;
+      default = "/var/lib/opensearch";
+      apply = converge (removeSuffix "/");
+      description = lib.mdDoc ''
+        Data directory for OpenSearch. If you change this, you need to
+        manually create the directory. You also need to create the
+        `opensearch` user and group, or change
+        [](#opt-services.opensearch.user) and
+        [](#opt-services.opensearch.group) to existing ones with
+        access to the directory.
+      '';
+    };
+
+    user = lib.mkOption {
+      type = lib.types.str;
+      default = "opensearch";
+      description = lib.mdDoc ''
+        The user OpenSearch runs as. Should be left at default unless
+        you have very specific needs.
+      '';
+    };
+
+    group = lib.mkOption {
+      type = lib.types.str;
+      default = "opensearch";
+      description = lib.mdDoc ''
+        The group OpenSearch runs as. Should be left at default unless
+        you have very specific needs.
+      '';
+    };
+
+    extraCmdLineOptions = lib.mkOption {
+      description = lib.mdDoc "Extra command line options for the OpenSearch launcher.";
+      default = [ ];
+      type = lib.types.listOf lib.types.str;
+    };
+
+    extraJavaOptions = lib.mkOption {
+      description = lib.mdDoc "Extra command line options for Java.";
+      default = [ ];
+      type = lib.types.listOf lib.types.str;
+      example = [ "-Djava.net.preferIPv4Stack=true" ];
+    };
+
+    restartIfChanged = lib.mkOption {
+      type = lib.types.bool;
+      description = lib.mdDoc ''
+        Automatically restart the service on config change.
+        This can be set to false to defer restarts on a server or cluster.
+        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 = true;
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.opensearch = {
+      description = "OpenSearch Daemon";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ pkgs.inetutils ];
+      inherit (cfg) restartIfChanged;
+      environment = {
+        OPENSEARCH_HOME = cfg.dataDir;
+        OPENSEARCH_JAVA_OPTS = toString cfg.extraJavaOptions;
+        OPENSEARCH_PATH_CONF = configDir;
+      };
+      serviceConfig = {
+        ExecStartPre =
+          let
+            startPreFullPrivileges = ''
+              set -o errexit -o pipefail -o nounset -o errtrace
+              shopt -s inherit_errexit
+            '' + (optionalString (!config.boot.isContainer) ''
+              # Only set vm.max_map_count if lower than ES required minimum
+              # This avoids conflict if configured via boot.kernel.sysctl
+              if [ $(${pkgs.procps}/bin/sysctl -n vm.max_map_count) -lt 262144 ]; then
+                ${pkgs.procps}/bin/sysctl -w vm.max_map_count=262144
+              fi
+            '');
+            startPreUnprivileged = ''
+              set -o errexit -o pipefail -o nounset -o errtrace
+              shopt -s inherit_errexit
+
+              # Install plugins
+              ln -sfT ${cfg.package}/lib ${cfg.dataDir}/lib
+              ln -sfT ${cfg.package}/modules ${cfg.dataDir}/modules
+
+              # opensearch needs to create the opensearch.keystore in the config directory
+              # so this directory needs to be writable.
+              mkdir -p ${configDir}
+              chmod 0700 ${configDir}
+
+              # Note that we copy config files from the nix store instead of symbolically linking them
+              # because otherwise X-Pack Security will raise the following exception:
+              # java.security.AccessControlException:
+              # access denied ("java.io.FilePermission" "/var/lib/opensearch/config/opensearch.yml" "read")
+
+              rm -f ${configDir}/opensearch.yml
+              cp ${opensearchYml} ${configDir}/opensearch.yml
+
+              # Make sure the logging configuration for old OpenSearch versions is removed:
+              rm -f "${configDir}/logging.yml"
+              rm -f ${configDir}/${loggingConfigFilename}
+              cp ${loggingConfigFile} ${configDir}/${loggingConfigFilename}
+              mkdir -p ${configDir}/scripts
+
+              rm -f ${configDir}/jvm.options
+              cp ${cfg.package}/config/jvm.options ${configDir}/jvm.options
+
+              # redirect jvm logs to the data directory
+              mkdir -p ${cfg.dataDir}/logs
+              chmod 0700 ${cfg.dataDir}/logs
+              sed -e '#logs/gc.log#${cfg.dataDir}/logs/gc.log#' -i ${configDir}/jvm.options
+            '';
+          in [
+            "+${pkgs.writeShellScript "opensearch-start-pre-full-privileges" startPreFullPrivileges}"
+            "${pkgs.writeShellScript "opensearch-start-pre-unprivileged" startPreUnprivileged}"
+          ];
+        ExecStartPost = pkgs.writeShellScript "opensearch-start-post" ''
+          set -o errexit -o pipefail -o nounset -o errtrace
+          shopt -s inherit_errexit
+
+          # Make sure opensearch is up and running before dependents
+          # are started
+          while ! ${pkgs.curl}/bin/curl -sS -f http://${cfg.settings."network.host"}:${toString cfg.settings."http.port"} 2>/dev/null; do
+            sleep 1
+          done
+        '';
+        ExecStart = "${cfg.package}/bin/opensearch ${toString cfg.extraCmdLineOptions}";
+        User = cfg.user;
+        Group = cfg.group;
+        LimitNOFILE = "1024000";
+        Restart = "always";
+        TimeoutStartSec = "infinity";
+        DynamicUser = usingDefaultUserAndGroup && usingDefaultDataDir;
+      } // (optionalAttrs (usingDefaultDataDir) {
+        StateDirectory = "opensearch";
+        StateDirectoryMode = "0700";
+      });
+    };
+
+    environment.systemPackages = [ cfg.package ];
+  };
+}
diff --git a/nixos/modules/services/search/qdrant.nix b/nixos/modules/services/search/qdrant.nix
new file mode 100644
index 00000000000..e1f7365d951
--- /dev/null
+++ b/nixos/modules/services/search/qdrant.nix
@@ -0,0 +1,129 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+
+  cfg = config.services.qdrant;
+
+  settingsFormat = pkgs.formats.yaml { };
+  configFile = settingsFormat.generate "config.yaml" cfg.settings;
+in {
+
+  options = {
+    services.qdrant = {
+      enable = mkEnableOption (lib.mdDoc "Vector Search Engine for the next generation of AI applications");
+
+      settings = mkOption {
+        description = lib.mdDoc ''
+          Configuration for Qdrant
+          Refer to <https://github.com/qdrant/qdrant/blob/master/config/config.yaml> for details on supported values.
+        '';
+
+        type = settingsFormat.type;
+
+        example = {
+          storage = {
+            storage_path = "/var/lib/qdrant/storage";
+            snapshots_path = "/var/lib/qdrant/snapshots";
+          };
+          hsnw_index = {
+            on_disk = true;
+          };
+          service = {
+            host = "127.0.0.1";
+            http_port = 6333;
+            grpc_port = 6334;
+          };
+          telemetry_disabled = true;
+        };
+
+        defaultText = literalExpression ''
+          {
+            storage = {
+              storage_path = "/var/lib/qdrant/storage";
+              snapshots_path = "/var/lib/qdrant/snapshots";
+            };
+            hsnw_index = {
+              on_disk = true;
+            };
+            service = {
+              host = "127.0.0.1";
+              http_port = 6333;
+              grpc_port = 6334;
+            };
+            telemetry_disabled = true;
+          }
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.qdrant.settings = {
+      storage.storage_path = mkDefault "/var/lib/qdrant/storage";
+      storage.snapshots_path = mkDefault "/var/lib/qdrant/snapshots";
+      # The following default values are the same as in the default config,
+      # they are just written here for convenience.
+      storage.on_disk_payload = mkDefault true;
+      storage.wal.wal_capacity_mb = mkDefault 32;
+      storage.wal.wal_segments_ahead = mkDefault 0;
+      storage.performance.max_search_threads = mkDefault 0;
+      storage.performance.max_optimization_threads = mkDefault 1;
+      storage.optimizers.deleted_threshold = mkDefault 0.2;
+      storage.optimizers.vacuum_min_vector_number = mkDefault 1000;
+      storage.optimizers.default_segment_number = mkDefault 0;
+      storage.optimizers.max_segment_size_kb = mkDefault null;
+      storage.optimizers.memmap_threshold_kb = mkDefault null;
+      storage.optimizers.indexing_threshold_kb = mkDefault 20000;
+      storage.optimizers.flush_interval_sec = mkDefault 5;
+      storage.optimizers.max_optimization_threads = mkDefault 1;
+      storage.hnsw_index.m = mkDefault 16;
+      storage.hnsw_index.ef_construct = mkDefault 100;
+      storage.hnsw_index.full_scan_threshold_kb = mkDefault 10000;
+      storage.hnsw_index.max_indexing_threads = mkDefault 0;
+      storage.hnsw_index.on_disk = mkDefault false;
+      storage.hnsw_index.payload_m = mkDefault null;
+      service.max_request_size_mb = mkDefault 32;
+      service.max_workers = mkDefault 0;
+      service.http_port = mkDefault 6333;
+      service.grpc_port = mkDefault 6334;
+      service.enable_cors = mkDefault true;
+      cluster.enabled = mkDefault false;
+      # the following have been altered for security
+      service.host = mkDefault "127.0.0.1";
+      telemetry_disabled = mkDefault true;
+    };
+
+    systemd.services.qdrant = {
+      description = "Vector Search Engine for the next generation of AI applications";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        LimitNOFILE=65536;
+        ExecStart = "${pkgs.qdrant}/bin/qdrant --config-path ${configFile}";
+        DynamicUser = true;
+        Restart = "on-failure";
+        StateDirectory = "qdrant";
+        CapabilityBoundingSet = "";
+        NoNewPrivileges = true;
+        PrivateTmp = true;
+        ProtectHome = true;
+        ProtectClock = true;
+        ProtectProc = "noaccess";
+        ProcSubset = "pid";
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        ProtectControlGroups = true;
+        ProtectHostname = true;
+        RestrictSUIDSGID = true;
+        RestrictRealtime = true;
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RemoveIPC = true;
+        SystemCallFilter = [ "@system-service" "~@privileged" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/search/solr.nix b/nixos/modules/services/search/solr.nix
deleted file mode 100644
index 05592e9fa24..00000000000
--- a/nixos/modules/services/search/solr.nix
+++ /dev/null
@@ -1,110 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.solr;
-
-in
-
-{
-  options = {
-    services.solr = {
-      enable = mkEnableOption (lib.mdDoc "Solr");
-
-      package = mkOption {
-        type = types.package;
-        default = pkgs.solr;
-        defaultText = literalExpression "pkgs.solr";
-        description = lib.mdDoc "Which Solr package to use.";
-      };
-
-      port = mkOption {
-        type = types.port;
-        default = 8983;
-        description = lib.mdDoc "Port on which Solr is ran.";
-      };
-
-      stateDir = mkOption {
-        type = types.path;
-        default = "/var/lib/solr";
-        description = lib.mdDoc "The solr home directory containing config, data, and logging files.";
-      };
-
-      extraJavaOptions = mkOption {
-        type = types.listOf types.str;
-        default = [];
-        description = lib.mdDoc "Extra command line options given to the java process running Solr.";
-      };
-
-      user = mkOption {
-        type = types.str;
-        default = "solr";
-        description = lib.mdDoc "User under which Solr is ran.";
-      };
-
-      group = mkOption {
-        type = types.str;
-        default = "solr";
-        description = lib.mdDoc "Group under which Solr is ran.";
-      };
-    };
-  };
-
-  config = mkIf cfg.enable {
-
-    environment.systemPackages = [ cfg.package ];
-
-    systemd.services.solr = {
-      after = [ "network.target" "remote-fs.target" "nss-lookup.target" "systemd-journald-dev-log.socket" ];
-      wantedBy = [ "multi-user.target" ];
-
-      environment = {
-        SOLR_HOME = "${cfg.stateDir}/data";
-        LOG4J_PROPS = "${cfg.stateDir}/log4j2.xml";
-        SOLR_LOGS_DIR = "${cfg.stateDir}/logs";
-        SOLR_PORT = "${toString cfg.port}";
-      };
-      path = with pkgs; [
-        gawk
-        procps
-      ];
-      preStart = ''
-        mkdir -p "${cfg.stateDir}/data";
-        mkdir -p "${cfg.stateDir}/logs";
-
-        if ! test -e "${cfg.stateDir}/data/solr.xml"; then
-          install -D -m0640 ${cfg.package}/server/solr/solr.xml "${cfg.stateDir}/data/solr.xml"
-          install -D -m0640 ${cfg.package}/server/solr/zoo.cfg "${cfg.stateDir}/data/zoo.cfg"
-        fi
-
-        if ! test -e "${cfg.stateDir}/log4j2.xml"; then
-          install -D -m0640 ${cfg.package}/server/resources/log4j2.xml "${cfg.stateDir}/log4j2.xml"
-        fi
-      '';
-
-      serviceConfig = {
-        User = cfg.user;
-        Group = cfg.group;
-        ExecStart="${cfg.package}/bin/solr start -f -a \"${concatStringsSep " " cfg.extraJavaOptions}\"";
-        ExecStop="${cfg.package}/bin/solr stop";
-      };
-    };
-
-    users.users = optionalAttrs (cfg.user == "solr") {
-      solr = {
-        group = cfg.group;
-        home = cfg.stateDir;
-        createHome = true;
-        uid = config.ids.uids.solr;
-      };
-    };
-
-    users.groups = optionalAttrs (cfg.group == "solr") {
-      solr.gid = config.ids.gids.solr;
-    };
-
-  };
-
-}
diff --git a/nixos/modules/services/security/aesmd.nix b/nixos/modules/services/security/aesmd.nix
index 7b0a46d6d02..8b3f010d7c4 100644
--- a/nixos/modules/services/security/aesmd.nix
+++ b/nixos/modules/services/security/aesmd.nix
@@ -25,6 +25,22 @@ in
       default = false;
       description = lib.mdDoc "Whether to build the PSW package in debug mode.";
     };
+    environment = mkOption {
+      type = with types; attrsOf str;
+      default = { };
+      description = mdDoc "Additional environment variables to pass to the AESM service.";
+      # Example environment variable for `sgx-azure-dcap-client` provider library
+      example = {
+        AZDCAP_COLLATERAL_VERSION = "v2";
+        AZDCAP_DEBUG_LOG_LEVEL = "INFO";
+      };
+    };
+    quoteProviderLibrary = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = literalExpression "pkgs.sgx-azure-dcap-client";
+      description = lib.mdDoc "Custom quote provider library to use.";
+    };
     settings = mkOption {
       description = lib.mdDoc "AESM configuration";
       default = { };
@@ -83,7 +99,6 @@ in
         storeAesmFolder = "${sgx-psw}/aesm";
         # Hardcoded path AESM_DATA_FOLDER in psw/ae/aesm_service/source/oal/linux/aesm_util.cpp
         aesmDataFolder = "/var/opt/aesmd/data";
-        aesmStateDirSystemd = "%S/aesmd";
       in
       {
         description = "Intel Architectural Enclave Service Manager";
@@ -98,8 +113,8 @@ in
         environment = {
           NAME = "aesm_service";
           AESM_PATH = storeAesmFolder;
-          LD_LIBRARY_PATH = storeAesmFolder;
-        };
+          LD_LIBRARY_PATH = makeLibraryPath [ cfg.quoteProviderLibrary ];
+        } // cfg.environment;
 
         # Make sure any of the SGX application enclave devices is available
         unitConfig.AssertPathExists = [
diff --git a/nixos/modules/services/security/authelia.nix b/nixos/modules/services/security/authelia.nix
new file mode 100644
index 00000000000..28c5fd0a1df
--- /dev/null
+++ b/nixos/modules/services/security/authelia.nix
@@ -0,0 +1,401 @@
+{ lib
+, pkgs
+, config
+, ...
+}:
+
+let
+  cfg = config.services.authelia;
+
+  format = pkgs.formats.yaml { };
+  configFile = format.generate "config.yml" cfg.settings;
+
+  autheliaOpts = with lib; { name, ... }: {
+    options = {
+      enable = mkEnableOption (mdDoc "Authelia instance");
+
+      name = mkOption {
+        type = types.str;
+        default = name;
+        description = mdDoc ''
+          Name is used as a suffix for the service name, user, and group.
+          By default it takes the value you use for `<instance>` in:
+          {option}`services.authelia.<instance>`
+        '';
+      };
+
+      package = mkOption {
+        default = pkgs.authelia;
+        type = types.package;
+        defaultText = literalExpression "pkgs.authelia";
+        description = mdDoc "Authelia derivation to use.";
+      };
+
+      user = mkOption {
+        default = "authelia-${name}";
+        type = types.str;
+        description = mdDoc "The name of the user for this authelia instance.";
+      };
+
+      group = mkOption {
+        default = "authelia-${name}";
+        type = types.str;
+        description = mdDoc "The name of the group for this authelia instance.";
+      };
+
+      secrets = mkOption {
+        description = mdDoc ''
+          It is recommended you keep your secrets separate from the configuration.
+          It's especially important to keep the raw secrets out of your nix configuration,
+          as the values will be preserved in your nix store.
+          This attribute allows you to configure the location of secret files to be loaded at runtime.
+
+          https://www.authelia.com/configuration/methods/secrets/
+        '';
+        default = { };
+        type = types.submodule {
+          options = {
+            manual = mkOption {
+              default = false;
+              example = true;
+              description = mdDoc ''
+                Configuring authelia's secret files via the secrets attribute set
+                is intended to be convenient and help catch cases where values are required
+                to run at all.
+                If a user wants to set these values themselves and bypass the validation they can set this value to true.
+              '';
+              type = types.bool;
+            };
+
+            # required
+            jwtSecretFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc ''
+                Path to your JWT secret used during identity verificaiton.
+              '';
+            };
+
+            oidcIssuerPrivateKeyFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc ''
+                Path to your private key file used to encrypt OIDC JWTs.
+              '';
+            };
+
+            oidcHmacSecretFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc ''
+                Path to your HMAC secret used to sign OIDC JWTs.
+              '';
+            };
+
+            sessionSecretFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc ''
+                Path to your session secret. Only used when redis is used as session storage.
+              '';
+            };
+
+            # required
+            storageEncryptionKeyFile = mkOption {
+              type = types.nullOr types.path;
+              default = null;
+              description = mdDoc ''
+                Path to your storage encryption key.
+              '';
+            };
+          };
+        };
+      };
+
+      environmentVariables = mkOption {
+        type = types.attrsOf types.str;
+        description = mdDoc ''
+          Additional environment variables to provide to authelia.
+          If you are providing secrets please consider the options under {option}`services.authelia.<instance>.secrets`
+          or make sure you use the `_FILE` suffix.
+          If you provide the raw secret rather than the location of a secret file that secret will be preserved in the nix store.
+          For more details: https://www.authelia.com/configuration/methods/secrets/
+        '';
+        default = { };
+      };
+
+      settings = mkOption {
+        description = mdDoc ''
+          Your Authelia config.yml as a Nix attribute set.
+          There are several values that are defined and documented in nix such as `default_2fa_method`,
+          but additional items can also be included.
+
+          https://github.com/authelia/authelia/blob/master/config.template.yml
+        '';
+        default = { };
+        example = ''
+          {
+            theme = "light";
+            default_2fa_method = "totp";
+            log.level = "debug";
+            server.disable_healthcheck = true;
+          }
+        '';
+        type = types.submodule {
+          freeformType = format.type;
+          options = {
+            theme = mkOption {
+              type = types.enum [ "light" "dark" "grey" "auto" ];
+              default = "light";
+              example = "dark";
+              description = mdDoc "The theme to display.";
+            };
+
+            default_2fa_method = mkOption {
+              type = types.enum [ "" "totp" "webauthn" "mobile_push" ];
+              default = "";
+              example = "webauthn";
+              description = mdDoc ''
+                Default 2FA method for new users and fallback for preferred but disabled methods.
+              '';
+            };
+
+            server = {
+              host = mkOption {
+                type = types.str;
+                default = "localhost";
+                example = "0.0.0.0";
+                description = mdDoc "The address to listen on.";
+              };
+
+              port = mkOption {
+                type = types.port;
+                default = 9091;
+                description = mdDoc "The port to listen on.";
+              };
+            };
+
+            log = {
+              level = mkOption {
+                type = types.enum [ "info" "debug" "trace" ];
+                default = "debug";
+                example = "info";
+                description = mdDoc "Level of verbosity for logs: info, debug, trace.";
+              };
+
+              format = mkOption {
+                type = types.enum [ "json" "text" ];
+                default = "json";
+                example = "text";
+                description = mdDoc "Format the logs are written as.";
+              };
+
+              file_path = mkOption {
+                type = types.nullOr types.path;
+                default = null;
+                example = "/var/log/authelia/authelia.log";
+                description = mdDoc "File path where the logs will be written. If not set logs are written to stdout.";
+              };
+
+              keep_stdout = mkOption {
+                type = types.bool;
+                default = false;
+                example = true;
+                description = mdDoc "Whether to also log to stdout when a `file_path` is defined.";
+              };
+            };
+
+            telemetry = {
+              metrics = {
+                enabled = mkOption {
+                  type = types.bool;
+                  default = false;
+                  example = true;
+                  description = mdDoc "Enable Metrics.";
+                };
+
+                address = mkOption {
+                  type = types.str;
+                  default = "tcp://127.0.0.1:9959";
+                  example = "tcp://0.0.0.0:8888";
+                  description = mdDoc "The address to listen on for metrics. This should be on a different port to the main `server.port` value.";
+                };
+              };
+            };
+          };
+        };
+      };
+
+      settingsFiles = mkOption {
+        type = types.listOf types.path;
+        default = [ ];
+        example = [ "/etc/authelia/config.yml" "/etc/authelia/access-control.yml" "/etc/authelia/config/" ];
+        description = mdDoc ''
+          Here you can provide authelia with configuration files or directories.
+          It is possible to give authelia multiple files and use the nix generated configuration
+          file set via {option}`services.authelia.<instance>.settings`.
+        '';
+      };
+    };
+  };
+in
+{
+  options.services.authelia.instances = with lib; mkOption {
+    default = { };
+    type = types.attrsOf (types.submodule autheliaOpts);
+    description = mdDoc ''
+      Multi-domain protection currently requires multiple instances of Authelia.
+      If you don't require multiple instances of Authelia you can define just the one.
+
+      https://www.authelia.com/roadmap/active/multi-domain-protection/
+    '';
+    example = ''
+      {
+        main = {
+          enable = true;
+          secrets.storageEncryptionKeyFile = "/etc/authelia/storageEncryptionKeyFile";
+          secrets.jwtSecretFile = "/etc/authelia/jwtSecretFile";
+          settings = {
+            theme = "light";
+            default_2fa_method = "totp";
+            log.level = "debug";
+            server.disable_healthcheck = true;
+          };
+        };
+        preprod = {
+          enable = false;
+          secrets.storageEncryptionKeyFile = "/mnt/pre-prod/authelia/storageEncryptionKeyFile";
+          secrets.jwtSecretFile = "/mnt/pre-prod/jwtSecretFile";
+          settings = {
+            theme = "dark";
+            default_2fa_method = "webauthn";
+            server.host = "0.0.0.0";
+          };
+        };
+        test.enable = true;
+        test.secrets.manual = true;
+        test.settings.theme = "grey";
+        test.settings.server.disable_healthcheck = true;
+        test.settingsFiles = [ "/mnt/test/authelia" "/mnt/test-authelia.conf" ];
+        };
+      }
+    '';
+  };
+
+  config =
+    let
+      mkInstanceServiceConfig = instance:
+        let
+          execCommand = "${instance.package}/bin/authelia";
+          configFile = format.generate "config.yml" instance.settings;
+          configArg = "--config ${builtins.concatStringsSep "," (lib.concatLists [[configFile] instance.settingsFiles])}";
+        in
+        {
+          description = "Authelia authentication and authorization server";
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+          environment =
+            (lib.filterAttrs (_: v: v != null) {
+              AUTHELIA_JWT_SECRET_FILE = instance.secrets.jwtSecretFile;
+              AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = instance.secrets.storageEncryptionKeyFile;
+              AUTHELIA_SESSION_SECRET_FILE = instance.secrets.sessionSecretFile;
+              AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = instance.secrets.oidcIssuerPrivateKeyFile;
+              AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = instance.secrets.oidcHmacSecretFile;
+            })
+            // instance.environmentVariables;
+
+          preStart = "${execCommand} ${configArg} validate-config";
+          serviceConfig = {
+            User = instance.user;
+            Group = instance.group;
+            ExecStart = "${execCommand} ${configArg}";
+            Restart = "always";
+            RestartSec = "5s";
+            StateDirectory = "authelia-${instance.name}";
+            StateDirectoryMode = "0700";
+
+            # Security options:
+            AmbientCapabilities = "";
+            CapabilityBoundingSet = "";
+            DeviceAllow = "";
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            NoNewPrivileges = true;
+
+            PrivateTmp = true;
+            PrivateDevices = true;
+            PrivateUsers = true;
+
+            ProtectClock = true;
+            ProtectControlGroups = true;
+            ProtectHome = "read-only";
+            ProtectHostname = true;
+            ProtectKernelLogs = true;
+            ProtectKernelModules = true;
+            ProtectKernelTunables = true;
+            ProtectProc = "noaccess";
+            ProtectSystem = "strict";
+
+            RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+            RestrictNamespaces = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+
+            SystemCallArchitectures = "native";
+            SystemCallErrorNumber = "EPERM";
+            SystemCallFilter = [
+              "@system-service"
+              "~@cpu-emulation"
+              "~@debug"
+              "~@keyring"
+              "~@memlock"
+              "~@obsolete"
+              "~@privileged"
+              "~@setuid"
+            ];
+          };
+        };
+      mkInstanceUsersConfig = instance: {
+        groups."authelia-${instance.name}" =
+          lib.mkIf (instance.group == "authelia-${instance.name}") {
+            name = "authelia-${instance.name}";
+          };
+        users."authelia-${instance.name}" =
+          lib.mkIf (instance.user == "authelia-${instance.name}") {
+            name = "authelia-${instance.name}";
+            isSystemUser = true;
+            group = instance.group;
+          };
+      };
+      instances = lib.attrValues cfg.instances;
+    in
+    {
+      assertions = lib.flatten (lib.flip lib.mapAttrsToList cfg.instances (name: instance:
+        [
+          {
+            assertion = instance.secrets.manual || (instance.secrets.jwtSecretFile != null && instance.secrets.storageEncryptionKeyFile != null);
+            message = ''
+              Authelia requires a JWT Secret and a Storage Encryption Key to work.
+              Either set them like so:
+              services.authelia.${name}.secrets.jwtSecretFile = /my/path/to/jwtsecret;
+              services.authelia.${name}.secrets.storageEncryptionKeyFile = /my/path/to/encryptionkey;
+              Or set services.authelia.${name}.secrets.manual = true and provide them yourself via
+              environmentVariables or settingsFiles.
+              Do not include raw secrets in nix settings.
+            '';
+          }
+        ]
+      ));
+
+      systemd.services = lib.mkMerge
+        (map
+          (instance: lib.mkIf instance.enable {
+            "authelia-${instance.name}" = mkInstanceServiceConfig instance;
+          })
+          instances);
+      users = lib.mkMerge
+        (map
+          (instance: lib.mkIf instance.enable (mkInstanceUsersConfig instance))
+          instances);
+    };
+}
diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix
index 3b124a4f0e0..eebf2a2f7b5 100644
--- a/nixos/modules/services/security/fail2ban.nix
+++ b/nixos/modules/services/security/fail2ban.nix
@@ -62,11 +62,10 @@ in
       };
 
       packageFirewall = mkOption {
-        default = pkgs.iptables;
-        defaultText = literalExpression "pkgs.iptables";
+        default = config.networking.firewall.package;
+        defaultText = literalExpression "config.networking.firewall.package";
         type = types.package;
-        example = literalExpression "pkgs.nftables";
-        description = lib.mdDoc "The firewall package used by fail2ban service.";
+        description = lib.mdDoc "The firewall package used by fail2ban service. Defaults to the package for your firewall (iptables or nftables).";
       };
 
       extraPackages = mkOption {
@@ -79,6 +78,13 @@ in
         '';
       };
 
+      bantime = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        example = "10m";
+        description = lib.mdDoc "Number of seconds that a host is banned.";
+      };
+
       maxretry = mkOption {
         default = 3;
         type = types.ints.unsigned;
@@ -86,24 +92,24 @@ in
       };
 
       banaction = mkOption {
-        default = "iptables-multiport";
+        default = if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport";
+        defaultText = literalExpression ''if config.networking.nftables.enable then "nftables-multiport" else "iptables-multiport"'';
         type = types.str;
-        example = "nftables-multiport";
         description = lib.mdDoc ''
           Default banning action (e.g. iptables, iptables-new, iptables-multiport,
-          iptables-ipset-proto6-allports, shorewall, etc) It is used to
+          iptables-ipset-proto6-allports, shorewall, etc). It is used to
           define action_* variables. Can be overridden globally or per
           section within jail.local file
         '';
       };
 
       banaction-allports = mkOption {
-        default = "iptables-allport";
+        default = if config.networking.nftables.enable then "nftables-allport" else "iptables-allport";
+        defaultText = literalExpression ''if config.networking.nftables.enable then "nftables-allport" else "iptables-allport"'';
         type = types.str;
-        example = "nftables-allport";
         description = lib.mdDoc ''
           Default banning action (e.g. iptables, iptables-new, iptables-multiport,
-          shorewall, etc) It is used to define action_* variables. Can be overridden
+          shorewall, etc) for "allports" jails. It is used to define action_* variables. Can be overridden
           globally or per section within jail.local file
         '';
       };
@@ -112,56 +118,56 @@ in
         default = false;
         type = types.bool;
         description = lib.mdDoc ''
-          Allows to use database for searching of previously banned ip's to increase
-          a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32...
+          "bantime.increment" allows to use database for searching of previously banned ip's to increase
+          a default ban time using special formula, default it is banTime * 1, 2, 4, 8, 16, 32 ...
         '';
       };
 
       bantime-increment.rndtime = mkOption {
-        default = "4m";
-        type = types.str;
+        default = null;
+        type = types.nullOr types.str;
         example = "8m";
         description = lib.mdDoc ''
-          "bantime-increment.rndtime" is the max number of seconds using for mixing with random time
+          "bantime.rndtime" is the max number of seconds using for mixing with random time
           to prevent "clever" botnets calculate exact time IP can be unbanned again
         '';
       };
 
       bantime-increment.maxtime = mkOption {
-        default = "10h";
-        type = types.str;
+        default = null;
+        type = types.nullOr types.str;
         example = "48h";
         description = lib.mdDoc ''
-          "bantime-increment.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
+          "bantime.maxtime" is the max number of seconds using the ban time can reach (don't grows further)
         '';
       };
 
       bantime-increment.factor = mkOption {
-        default = "1";
-        type = types.str;
+        default = null;
+        type = types.nullOr types.str;
         example = "4";
         description = lib.mdDoc ''
-          "bantime-increment.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
+          "bantime.factor" is a coefficient to calculate exponent growing of the formula or common multiplier,
           default value of factor is 1 and with default value of formula, the ban time grows by 1, 2, 4, 8, 16 ...
         '';
       };
 
       bantime-increment.formula = mkOption {
-        default = "ban.Time * (1<<(ban.Count if ban.Count<20 else 20)) * banFactor";
-        type = types.str;
+        default = null;
+        type = types.nullOr types.str;
         example = "ban.Time * math.exp(float(ban.Count+1)*banFactor)/math.exp(1*banFactor)";
         description = lib.mdDoc ''
-          "bantime-increment.formula" used by default to calculate next value of ban time, default value bellow,
-          the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32...
+          "bantime.formula" used by default to calculate next value of ban time, default value bellow,
+          the same ban time growing will be reached by multipliers 1, 2, 4, 8, 16, 32 ...
         '';
       };
 
       bantime-increment.multipliers = mkOption {
-        default = "1 2 4 8 16 32 64";
-        type = types.str;
-        example = "2 4 16 128";
+        default = null;
+        type = types.nullOr types.str;
+        example = "1 2 4 8 16 32 64";
         description = lib.mdDoc ''
-          "bantime-increment.multipliers" used to calculate next value of ban time instead of formula, corresponding
+          "bantime.multipliers" used to calculate next value of ban time instead of formula, corresponding
           previously ban count and given "bantime.factor" (for multipliers default is 1);
           following example grows ban time by 1, 2, 4, 8, 16 ... and if last ban count greater as multipliers count,
           always used last multiplier (64 in example), for factor '1' and original ban time 600 - 10.6 hours
@@ -169,11 +175,11 @@ in
       };
 
       bantime-increment.overalljails = mkOption {
-        default = false;
-        type = types.bool;
+        default = null;
+        type = types.nullOr types.bool;
         example = true;
         description = lib.mdDoc ''
-          "bantime-increment.overalljails"  (if true) specifies the search of IP in the database will be executed
+          "bantime.overalljails" (if true) specifies the search of IP in the database will be executed
           cross over all jails, if false (default), only current jail of the ban IP will be searched
         '';
       };
@@ -203,6 +209,20 @@ in
        '';
       };
 
+      extraSettings = mkOption {
+        type = with types; attrsOf (oneOf [ bool ints.positive str ]);
+        default = {};
+        description = lib.mdDoc ''
+          Extra default configuration for all jails (i.e. `[DEFAULT]`). See
+          <https://github.com/fail2ban/fail2ban/blob/master/config/jail.conf> for an overview.
+        '';
+        example = literalExpression ''
+          {
+            findtime = "15m";
+          }
+        '';
+      };
+
       jails = mkOption {
         default = { };
         example = literalExpression ''
@@ -256,8 +276,16 @@ in
   ###### implementation
 
   config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = (cfg.bantime-increment.formula == null || cfg.bantime-increment.multipliers == null);
+        message = ''
+          Options `services.fail2ban.bantime-increment.formula` and `services.fail2ban.bantime-increment.multipliers` cannot be both specified.
+        '';
+      }
+    ];
 
-    warnings = mkIf (config.networking.firewall.enable == false && config.networking.nftables.enable == false) [
+    warnings = mkIf (!config.networking.firewall.enable && !config.networking.nftables.enable) [
       "fail2ban can not be used without a firewall"
     ];
 
@@ -274,26 +302,16 @@ in
       "fail2ban/filter.d".source = "${cfg.package}/etc/fail2ban/filter.d/*.conf";
     };
 
+    systemd.packages = [ cfg.package ];
     systemd.services.fail2ban = {
-      description = "Fail2ban Intrusion Prevention System";
-
       wantedBy = [ "multi-user.target" ];
-      after = [ "network.target" ];
       partOf = optional config.networking.firewall.enable "firewall.service";
 
       restartTriggers = [ fail2banConf jailConf pathsConf ];
 
       path = [ cfg.package cfg.packageFirewall pkgs.iproute2 ] ++ cfg.extraPackages;
 
-      unitConfig.Documentation = "man:fail2ban(1)";
-
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/fail2ban-server -xf start";
-        ExecStop = "${cfg.package}/bin/fail2ban-server stop";
-        ExecReload = "${cfg.package}/bin/fail2ban-server reload";
-        Type = "simple";
-        Restart = "on-failure";
-        PIDFile = "/run/fail2ban/fail2ban.pid";
         # Capabilities
         CapabilityBoundingSet = [ "CAP_AUDIT_READ" "CAP_DAC_READ_SEARCH" "CAP_NET_ADMIN" "CAP_NET_RAW" ];
         # Security
@@ -320,27 +338,33 @@ in
     # Add some reasonable default jails.  The special "DEFAULT" jail
     # sets default values for all other jails.
     services.fail2ban.jails.DEFAULT = ''
-      ${optionalString cfg.bantime-increment.enable ''
-        # Bantime incremental
-        bantime.increment    = ${boolToString cfg.bantime-increment.enable}
-        bantime.maxtime      = ${cfg.bantime-increment.maxtime}
-        bantime.factor       = ${cfg.bantime-increment.factor}
-        bantime.formula      = ${cfg.bantime-increment.formula}
-        bantime.multipliers  = ${cfg.bantime-increment.multipliers}
-        bantime.overalljails = ${boolToString cfg.bantime-increment.overalljails}
-      ''}
+      # Bantime increment options
+      bantime.increment = ${boolToString cfg.bantime-increment.enable}
+      ${optionalString (cfg.bantime-increment.rndtime != null) "bantime.rndtime = ${cfg.bantime-increment.rndtime}"}
+      ${optionalString (cfg.bantime-increment.maxtime != null) "bantime.maxtime = ${cfg.bantime-increment.maxtime}"}
+      ${optionalString (cfg.bantime-increment.factor != null) "bantime.factor = ${cfg.bantime-increment.factor}"}
+      ${optionalString (cfg.bantime-increment.formula != null) "bantime.formula = ${cfg.bantime-increment.formula}"}
+      ${optionalString (cfg.bantime-increment.multipliers != null) "bantime.multipliers = ${cfg.bantime-increment.multipliers}"}
+      ${optionalString (cfg.bantime-increment.overalljails != null) "bantime.overalljails = ${boolToString cfg.bantime-increment.overalljails}"}
       # Miscellaneous options
       ignoreip    = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP}
+      ${optionalString (cfg.bantime != null) ''
+        bantime     = ${cfg.bantime}
+      ''}
       maxretry    = ${toString cfg.maxretry}
       backend     = systemd
       # Actions
       banaction   = ${cfg.banaction}
       banaction_allports = ${cfg.banaction-allports}
+      ${optionalString (cfg.extraSettings != {}) ''
+        # Extra settings
+        ${generators.toKeyValue {} cfg.extraSettings}
+      ''}
     '';
     # Block SSH if there are too many failing connection attempts.
     # Benefits from verbose sshd logging to observe failed login attempts,
     # so we set that here unless the user overrode it.
-    services.openssh.logLevel = lib.mkDefault "VERBOSE";
+    services.openssh.settings.LogLevel = lib.mkDefault "VERBOSE";
     services.fail2ban.jails.sshd = mkDefault ''
       enabled = true
       port    = ${concatMapStringsSep "," (p: toString p) config.services.openssh.ports}
diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix
index 55120799c99..2f19decb5cb 100644
--- a/nixos/modules/services/security/kanidm.nix
+++ b/nixos/modules/services/security/kanidm.nix
@@ -7,6 +7,18 @@ let
   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);
+  certPaths = builtins.map builtins.dirOf [ cfg.serverSettings.tls_chain cfg.serverSettings.tls_key ];
+
+  # Merge bind mount paths and remove paths where a prefix is already mounted.
+  # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is alread in the mount
+  # paths, no new bind mount is added. Adding subpaths caused problems on ofborg.
+  hasPrefixInList = list: newPath: lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list;
+  mergePaths = lib.foldl' (merged: newPath: let
+      # If the new path is a prefix to some existing path, we need to filter it out
+      filteredPaths = lib.filter (p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p)) merged;
+      # If a prefix of the new path is already in the list, do not add it
+      filteredNew = if hasPrefixInList filteredPaths newPath then [] else [ newPath ];
+    in filteredPaths ++ filteredNew) [];
 
   defaultServiceConfig = {
     BindReadOnlyPaths = [
@@ -16,7 +28,7 @@ let
       "-/etc/hosts"
       "-/etc/localtime"
     ];
-    CapabilityBoundingSet = "";
+    CapabilityBoundingSet = [];
     # ProtectClock= adds DeviceAllow=char-rtc r
     DeviceAllow = "";
     # Implies ProtectSystem=strict, which re-mounts all paths
@@ -55,7 +67,7 @@ in
   options.services.kanidm = {
     enableClient = lib.mkEnableOption (lib.mdDoc "the Kanidm client");
     enableServer = lib.mkEnableOption (lib.mdDoc "the Kanidm server");
-    enablePam = lib.mkEnableOption (lib.mdDoc "the Kanidm PAM and NSS integration.");
+    enablePam = lib.mkEnableOption (lib.mdDoc "the Kanidm PAM and NSS integration");
 
     serverSettings = lib.mkOption {
       type = lib.types.submodule {
@@ -216,22 +228,28 @@ in
       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";
+      serviceConfig = lib.mkMerge [
+        # Merge paths and ignore existing prefixes needs to sidestep mkMerge
+        (defaultServiceConfig // {
+          BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths);
+        })
+        {
+          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";
-      };
+          AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+          CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+          # This would otherwise override the CAP_NET_BIND_SERVICE capability.
+          PrivateUsers = lib.mkForce false;
+          # Port needs to be exposed to the host network
+          PrivateNetwork = lib.mkForce false;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+          TemporaryFileSystem = "/:ro";
+        }
+      ];
       environment.RUST_LOG = "info";
     };
 
@@ -240,34 +258,32 @@ in
       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";
+      serviceConfig = lib.mkMerge [
+        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"
-          "-/etc/ssl"
-          "-/etc/static/ssl"
-        ];
-        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";
-      };
+          BindReadOnlyPaths = [
+            "-/etc/kanidm"
+            "-/etc/static/kanidm"
+            "-/etc/ssl"
+            "-/etc/static/ssl"
+          ];
+          BindPaths = [
+            # To create the socket
+            "/run/kanidm-unixd:/var/run/kanidm-unixd"
+          ];
+          # Needs to connect to kanidmd
+          PrivateNetwork = lib.mkForce false;
+          RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
+          TemporaryFileSystem = "/:ro";
+        }
+      ];
       environment.RUST_LOG = "info";
     };
 
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
index e3f8e75ca24..12547acabfe 100644
--- a/nixos/modules/services/security/oauth2_proxy.nix
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -72,15 +72,14 @@ let
   } // (getProviderOptions cfg cfg.provider) // cfg.extraConfig;
 
   mapConfig = key: attr:
-  if attr != null && attr != [] then (
+  optionalString (attr != null && attr != []) (
     if isDerivation attr then mapConfig key (toString attr) else
     if (builtins.typeOf attr) == "set" then concatStringsSep " "
       (mapAttrsToList (name: value: mapConfig (key + "-" + name) value) attr) else
     if (builtins.typeOf attr) == "list" then concatMapStringsSep " " (mapConfig key) attr else
     if (builtins.typeOf attr) == "bool" then "--${key}=${boolToString attr}" else
     if (builtins.typeOf attr) == "string" then "--${key}='${attr}'" else
-    "--${key}=${toString attr}")
-    else "";
+    "--${key}=${toString attr}");
 
   configString = concatStringsSep " " (mapAttrsToList mapConfig allConfig);
 in
diff --git a/nixos/modules/services/security/privacyidea.nix b/nixos/modules/services/security/privacyidea.nix
index e446e606cad..664335cb58e 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" ]; python3 = pkgs.python39; };
+  uwsgi = pkgs.uwsgi.override { plugins = [ "python3" ]; python3 = pkgs.python310; };
   python = uwsgi.python3;
   penv = python.withPackages (const [ pkgs.privacyidea ]);
   logCfg = pkgs.writeText "privacyidea-log.cfg" ''
@@ -41,7 +41,7 @@ let
 
   piCfgFile = pkgs.writeText "privacyidea.cfg" ''
     SUPERUSER_REALM = [ '${concatStringsSep "', '" cfg.superuserRealm}' ]
-    SQLALCHEMY_DATABASE_URI = 'postgresql:///privacyidea'
+    SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2:///privacyidea'
     SECRET_KEY = '${cfg.secretKey}'
     PI_PEPPER = '${cfg.pepper}'
     PI_ENCFILE = '${cfg.encFile}'
diff --git a/nixos/modules/services/security/vault-agent.nix b/nixos/modules/services/security/vault-agent.nix
new file mode 100644
index 00000000000..244004f7c91
--- /dev/null
+++ b/nixos/modules/services/security/vault-agent.nix
@@ -0,0 +1,128 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  format = pkgs.formats.json { };
+  commonOptions = { pkgName, flavour ? pkgName }: mkOption {
+    default = { };
+    description = mdDoc ''
+      Attribute set of ${flavour} instances.
+      Creates independent `${flavour}-''${name}.service` systemd units for each instance defined here.
+    '';
+    type = with types; attrsOf (submodule ({ name, ... }: {
+      options = {
+        enable = mkEnableOption (mdDoc "this ${flavour} instance") // { default = true; };
+
+        package = mkPackageOptionMD pkgs pkgName { };
+
+        user = mkOption {
+          type = types.str;
+          default = "root";
+          description = mdDoc ''
+            User under which this instance runs.
+          '';
+        };
+
+        group = mkOption {
+          type = types.str;
+          default = "root";
+          description = mdDoc ''
+            Group under which this instance runs.
+          '';
+        };
+
+        settings = mkOption {
+          type = types.submodule {
+            freeformType = format.type;
+
+            options = {
+              pid_file = mkOption {
+                default = "/run/${flavour}/${name}.pid";
+                type = types.str;
+                description = mdDoc ''
+                  Path to use for the pid file.
+                '';
+              };
+
+              template = mkOption {
+                default = [ ];
+                type = with types; listOf (attrsOf anything);
+                description =
+                  let upstreamDocs =
+                    if flavour == "vault-agent"
+                    then "https://developer.hashicorp.com/vault/docs/agent/template"
+                    else "https://github.com/hashicorp/consul-template/blob/main/docs/configuration.md#templates";
+                  in
+                  mdDoc ''
+                    Template section of ${flavour}.
+                    Refer to <${upstreamDocs}> for supported values.
+                  '';
+              };
+            };
+          };
+
+          default = { };
+
+          description =
+            let upstreamDocs =
+              if flavour == "vault-agent"
+              then "https://developer.hashicorp.com/vault/docs/agent#configuration-file-options"
+              else "https://github.com/hashicorp/consul-template/blob/main/docs/configuration.md#configuration-file";
+            in
+            mdDoc ''
+              Free-form settings written directly to the `config.json` file.
+              Refer to <${upstreamDocs}> for supported values.
+
+              ::: {.note}
+              Resulting format is JSON not HCL.
+              Refer to <https://www.hcl2json.com/> if you are unsure how to convert HCL options to JSON.
+              :::
+            '';
+        };
+      };
+    }));
+  };
+
+  createAgentInstance = { instance, name, flavour }:
+    let
+      configFile = format.generate "${name}.json" instance.settings;
+    in
+    mkIf (instance.enable) {
+      description = "${flavour} daemon - ${name}";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      path = [ pkgs.getent ];
+      startLimitIntervalSec = 60;
+      startLimitBurst = 3;
+      serviceConfig = {
+        User = instance.user;
+        Group = instance.group;
+        RuntimeDirectory = flavour;
+        ExecStart = "${getExe instance.package} ${optionalString ((getName instance.package) == "vault") "agent"} -config ${configFile}";
+        ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
+        KillSignal = "SIGINT";
+        TimeoutStopSec = "30s";
+        Restart = "on-failure";
+      };
+    };
+in
+{
+  options = {
+    services.consul-template.instances = commonOptions { pkgName = "consul-template"; };
+    services.vault-agent.instances = commonOptions { pkgName = "vault"; flavour = "vault-agent"; };
+  };
+
+  config = mkMerge (map
+    (flavour:
+      let cfg = config.services.${flavour}; in
+      mkIf (cfg.instances != { }) {
+        systemd.services = mapAttrs'
+          (name: instance: nameValuePair "${flavour}-${name}" (createAgentInstance { inherit name instance flavour; }))
+          cfg.instances;
+      })
+    [ "consul-template" "vault-agent" ]);
+
+  meta.maintainers = with maintainers; [ indeednotjames tcheronneau ];
+}
+
diff --git a/nixos/modules/services/security/yubikey-agent.nix b/nixos/modules/services/security/yubikey-agent.nix
index c91ff3e69a0..ee57ec8bf81 100644
--- a/nixos/modules/services/security/yubikey-agent.nix
+++ b/nixos/modules/services/security/yubikey-agent.nix
@@ -57,6 +57,9 @@ in
       ];
     };
 
+    # Yubikey-agent expects pcsd to be running in order to function.
+    services.pcscd.enable = true;
+
     environment.extraInit = ''
       if [ -z "$SSH_AUTH_SOCK" -a -n "$XDG_RUNTIME_DIR" ]; then
         export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/yubikey-agent/yubikey-agent.sock"
diff --git a/nixos/modules/services/system/cachix-agent/default.nix b/nixos/modules/services/system/cachix-agent/default.nix
index aa3b2153422..06494ddb631 100644
--- a/nixos/modules/services/system/cachix-agent/default.nix
+++ b/nixos/modules/services/system/cachix-agent/default.nix
@@ -67,11 +67,12 @@ in {
       serviceConfig = {
         # we don't want to kill children processes as those are deployments
         KillMode = "process";
-        Restart = "on-failure";
+        Restart = "always";
+        RestartSec = 5;
         EnvironmentFile = cfg.credentialsFile;
         ExecStart = ''
           ${cfg.package}/bin/cachix ${lib.optionalString cfg.verbose "--verbose"} ${lib.optionalString (cfg.host != null) "--host ${cfg.host}"} \
-            deploy agent ${cfg.name} ${if cfg.profile != null then cfg.profile else ""}
+            deploy agent ${cfg.name} ${optionalString (cfg.profile != null) cfg.profile}
         '';
       };
     };
diff --git a/nixos/modules/services/system/cachix-watch-store.nix b/nixos/modules/services/system/cachix-watch-store.nix
index ec73c0bcdcf..89157b460b9 100644
--- a/nixos/modules/services/system/cachix-watch-store.nix
+++ b/nixos/modules/services/system/cachix-watch-store.nix
@@ -25,7 +25,7 @@ in
 
     compressionLevel = mkOption {
       type = types.nullOr types.int;
-      description = lib.mdDoc "The compression level for XZ compression (between 0 and 9)";
+      description = lib.mdDoc "The compression level for ZSTD compression (between 0 and 16)";
       default = null;
     };
 
@@ -62,7 +62,13 @@ in
       after = [ "network-online.target" ];
       path = [ config.nix.package ];
       wantedBy = [ "multi-user.target" ];
+      unitConfig = {
+        # allow to restart indefinitely
+        StartLimitIntervalSec = 0;
+      };
       serviceConfig = {
+        # don't put too much stress on the machine when restarting
+        RestartSec = 1;
         # we don't want to kill children processes as those are deployments
         KillMode = "process";
         Restart = "on-failure";
diff --git a/nixos/modules/services/system/cloud-init.nix b/nixos/modules/services/system/cloud-init.nix
index d75070dea43..8c932b0e6f7 100644
--- a/nixos/modules/services/system/cloud-init.nix
+++ b/nixos/modules/services/system/cloud-init.nix
@@ -2,17 +2,22 @@
 
 with lib;
 
-let cfg = config.services.cloud-init;
-    path = with pkgs; [
-      cloud-init
-      iproute2
-      nettools
-      openssh
-      shadow
-      util-linux
-    ] ++ optional cfg.btrfs.enable btrfs-progs
-      ++ optional cfg.ext4.enable e2fsprogs
-    ;
+let
+  cfg = config.services.cloud-init;
+  path = with pkgs; [
+    cloud-init
+    iproute2
+    nettools
+    openssh
+    shadow
+    util-linux
+    busybox
+  ]
+  ++ optional cfg.btrfs.enable btrfs-progs
+  ++ optional cfg.ext4.enable e2fsprogs
+  ;
+  settingsFormat = pkgs.formats.yaml { };
+  cfgfile = settingsFormat.generate "cloud.cfg" cfg.settings;
 in
 {
   options = {
@@ -20,7 +25,7 @@ in
       enable = mkOption {
         type = types.bool;
         default = false;
-        description = lib.mdDoc ''
+        description = mdDoc ''
           Enable the cloud-init service. This services reads
           configuration metadata in a cloud environment and configures
           the machine according to this metadata.
@@ -39,7 +44,7 @@ in
       btrfs.enable = mkOption {
         type = types.bool;
         default = false;
-        description = lib.mdDoc ''
+        description = mdDoc ''
           Allow the cloud-init service to operate `btrfs` filesystem.
         '';
       };
@@ -47,7 +52,7 @@ in
       ext4.enable = mkOption {
         type = types.bool;
         default = true;
-        description = lib.mdDoc ''
+        description = mdDoc ''
           Allow the cloud-init service to operate `ext4` filesystem.
         '';
       };
@@ -55,141 +60,170 @@ in
       network.enable = mkOption {
         type = types.bool;
         default = false;
-        description = lib.mdDoc ''
+        description = mdDoc ''
           Allow the cloud-init service to configure network interfaces
           through systemd-networkd.
         '';
       };
 
+      settings = mkOption {
+        description = mdDoc ''
+          Structured cloud-init configuration.
+        '';
+        type = types.submodule {
+          freeformType = settingsFormat.type;
+        };
+        default = { };
+      };
+
       config = mkOption {
         type = types.str;
-        default = ''
-          system_info:
-            distro: nixos
-            network:
-              renderers: [ 'networkd' ]
-          users:
-             - root
-
-          disable_root: false
-          preserve_hostname: false
-
-          cloud_init_modules:
-           - migrator
-           - seed_random
-           - bootcmd
-           - write-files
-           - growpart
-           - resizefs
-           - update_hostname
-           - resolv_conf
-           - ca-certs
-           - rsyslog
-           - users-groups
-
-          cloud_config_modules:
-           - disk_setup
-           - mounts
-           - ssh-import-id
-           - set-passwords
-           - timezone
-           - disable-ec2-metadata
-           - runcmd
-           - ssh
-
-          cloud_final_modules:
-           - rightscale_userdata
-           - scripts-vendor
-           - scripts-per-once
-           - scripts-per-boot
-           - scripts-per-instance
-           - scripts-user
-           - ssh-authkey-fingerprints
-           - keys-to-console
-           - phone-home
-           - final-message
-           - power-state-change
-          '';
-        description = lib.mdDoc "cloud-init configuration.";
+        default = "";
+        description = mdDoc ''
+          raw cloud-init configuration.
+
+          Takes precedence over the `settings` option if set.
+        '';
       };
 
     };
 
   };
 
-  config = mkIf cfg.enable {
+  config = {
+    services.cloud-init.settings = {
+      system_info = mkDefault {
+        distro = "nixos";
+        network = {
+          renderers = [ "networkd" ];
+        };
+      };
 
-    environment.etc."cloud/cloud.cfg".text = cfg.config;
+      users = mkDefault [ "root" ];
+      disable_root = mkDefault false;
+      preserve_hostname = mkDefault false;
+
+      cloud_init_modules = mkDefault [
+        "migrator"
+        "seed_random"
+        "bootcmd"
+        "write-files"
+        "growpart"
+        "resizefs"
+        "update_hostname"
+        "resolv_conf"
+        "ca-certs"
+        "rsyslog"
+        "users-groups"
+      ];
+
+      cloud_config_modules = mkDefault [
+        "disk_setup"
+        "mounts"
+        "ssh-import-id"
+        "set-passwords"
+        "timezone"
+        "disable-ec2-metadata"
+        "runcmd"
+        "ssh"
+      ];
+
+      cloud_final_modules = mkDefault [
+        "rightscale_userdata"
+        "scripts-vendor"
+        "scripts-per-once"
+        "scripts-per-boot"
+        "scripts-per-instance"
+        "scripts-user"
+        "ssh-authkey-fingerprints"
+        "keys-to-console"
+        "phone-home"
+        "final-message"
+        "power-state-change"
+      ];
+    };
+  } // (mkIf cfg.enable {
+
+    environment.etc."cloud/cloud.cfg" =
+      if cfg.config == "" then
+        { source = cfgfile; }
+      else
+        { text = cfg.config; }
+    ;
 
     systemd.network.enable = cfg.network.enable;
 
-    systemd.services.cloud-init-local =
-      { description = "Initial cloud-init job (pre-networking)";
-        wantedBy = [ "multi-user.target" ];
-        before = ["systemd-networkd.service"];
-        path = path;
-        serviceConfig =
-          { Type = "oneshot";
-            ExecStart = "${pkgs.cloud-init}/bin/cloud-init init --local";
-            RemainAfterExit = "yes";
-            TimeoutSec = "infinity";
-            StandardOutput = "journal+console";
-          };
+    systemd.services.cloud-init-local = {
+      description = "Initial cloud-init job (pre-networking)";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "systemd-networkd.service" ];
+      path = path;
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.cloud-init}/bin/cloud-init init --local";
+        RemainAfterExit = "yes";
+        TimeoutSec = "infinity";
+        StandardOutput = "journal+console";
       };
+    };
 
-    systemd.services.cloud-init =
-      { description = "Initial cloud-init job (metadata service crawler)";
-        wantedBy = [ "multi-user.target" ];
-        wants = [ "network-online.target" "cloud-init-local.service"
-                  "sshd.service" "sshd-keygen.service" ];
-        after = [ "network-online.target" "cloud-init-local.service" ];
-        before = [ "sshd.service" "sshd-keygen.service" ];
-        requires = [ "network.target"];
-        path = path;
-        serviceConfig =
-          { Type = "oneshot";
-            ExecStart = "${pkgs.cloud-init}/bin/cloud-init init";
-            RemainAfterExit = "yes";
-            TimeoutSec = "infinity";
-            StandardOutput = "journal+console";
-          };
+    systemd.services.cloud-init = {
+      description = "Initial cloud-init job (metadata service crawler)";
+      wantedBy = [ "multi-user.target" ];
+      wants = [
+        "network-online.target"
+        "cloud-init-local.service"
+        "sshd.service"
+        "sshd-keygen.service"
+      ];
+      after = [ "network-online.target" "cloud-init-local.service" ];
+      before = [ "sshd.service" "sshd-keygen.service" ];
+      requires = [ "network.target" ];
+      path = path;
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.cloud-init}/bin/cloud-init init";
+        RemainAfterExit = "yes";
+        TimeoutSec = "infinity";
+        StandardOutput = "journal+console";
       };
+    };
 
-    systemd.services.cloud-config =
-      { description = "Apply the settings specified in cloud-config";
-        wantedBy = [ "multi-user.target" ];
-        wants = [ "network-online.target" ];
-        after = [ "network-online.target" "syslog.target" "cloud-config.target" ];
-
-        path = path;
-        serviceConfig =
-          { Type = "oneshot";
-            ExecStart = "${pkgs.cloud-init}/bin/cloud-init modules --mode=config";
-            RemainAfterExit = "yes";
-            TimeoutSec = "infinity";
-            StandardOutput = "journal+console";
-          };
+    systemd.services.cloud-config = {
+      description = "Apply the settings specified in cloud-config";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" "syslog.target" "cloud-config.target" ];
+
+      path = path;
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.cloud-init}/bin/cloud-init modules --mode=config";
+        RemainAfterExit = "yes";
+        TimeoutSec = "infinity";
+        StandardOutput = "journal+console";
       };
+    };
 
-    systemd.services.cloud-final =
-      { description = "Execute cloud user/final scripts";
-        wantedBy = [ "multi-user.target" ];
-        wants = [ "network-online.target" ];
-        after = [ "network-online.target" "syslog.target" "cloud-config.service" "rc-local.service" ];
-        requires = [ "cloud-config.target" ];
-        path = path;
-        serviceConfig =
-          { Type = "oneshot";
-            ExecStart = "${pkgs.cloud-init}/bin/cloud-init modules --mode=final";
-            RemainAfterExit = "yes";
-            TimeoutSec = "infinity";
-            StandardOutput = "journal+console";
-          };
+    systemd.services.cloud-final = {
+      description = "Execute cloud user/final scripts";
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" "syslog.target" "cloud-config.service" "rc-local.service" ];
+      requires = [ "cloud-config.target" ];
+      path = path;
+      serviceConfig = {
+        Type = "oneshot";
+        ExecStart = "${pkgs.cloud-init}/bin/cloud-init modules --mode=final";
+        RemainAfterExit = "yes";
+        TimeoutSec = "infinity";
+        StandardOutput = "journal+console";
       };
+    };
 
-    systemd.targets.cloud-config =
-      { description = "Cloud-config availability";
-        requires = [ "cloud-init-local.service" "cloud-init.service" ];
-      };
-  };
+    systemd.targets.cloud-config = {
+      description = "Cloud-config availability";
+      requires = [ "cloud-init-local.service" "cloud-init.service" ];
+    };
+  });
 }
diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix
index c677088101f..9d8a62ec78c 100644
--- a/nixos/modules/services/system/dbus.nix
+++ b/nixos/modules/services/system/dbus.nix
@@ -14,13 +14,17 @@ let
     serviceDirectories = cfg.packages;
   };
 
-  inherit (lib) mkOption mkIf mkMerge types;
+  inherit (lib) mkOption mkEnableOption mkIf mkMerge types;
 
 in
 
 {
   options = {
 
+    boot.initrd.systemd.dbus = {
+      enable = mkEnableOption (lib.mdDoc "dbus in stage 1") // { visible = false; };
+    };
+
     services.dbus = {
 
       enable = mkOption {
@@ -111,6 +115,21 @@ in
       ];
     }
 
+    (mkIf config.boot.initrd.systemd.dbus.enable {
+      boot.initrd.systemd = {
+        users.messagebus = { };
+        groups.messagebus = { };
+        contents."/etc/dbus-1".source = pkgs.makeDBusConf {
+          inherit (cfg) apparmor;
+          suidHelper = "/bin/false";
+          serviceDirectories = [ pkgs.dbus ];
+        };
+        packages = [ pkgs.dbus ];
+        storePaths = [ "${pkgs.dbus}/bin/dbus-daemon" ];
+        targets.sockets.wants = [ "dbus.socket" ];
+      };
+    })
+
     (mkIf (cfg.implementation == "dbus") {
       environment.systemPackages = [
         pkgs.dbus
diff --git a/nixos/modules/services/system/nscd.nix b/nixos/modules/services/system/nscd.nix
index fdc5190d084..971dffbadc1 100644
--- a/nixos/modules/services/system/nscd.nix
+++ b/nixos/modules/services/system/nscd.nix
@@ -29,10 +29,11 @@ in
 
       enableNsncd = mkOption {
         type = types.bool;
-        default = false;
+        default = true;
         description = lib.mdDoc ''
-          Whether to use nsncd instead of nscd.
+          Whether to use nsncd instead of nscd from glibc.
           This is a nscd-compatible daemon, that proxies lookups, without any caching.
+          Using nscd from glibc is discouraged.
         '';
       };
 
@@ -55,7 +56,10 @@ in
       config = mkOption {
         type = types.lines;
         default = builtins.readFile ./nscd.conf;
-        description = lib.mdDoc "Configuration to use for Name Service Cache Daemon.";
+        description = lib.mdDoc ''
+          Configuration to use for Name Service Cache Daemon.
+          Only used in case glibc-nscd is used.
+        '';
       };
 
       package = mkOption {
diff --git a/nixos/modules/services/system/self-deploy.nix b/nixos/modules/services/system/self-deploy.nix
index 9b1ebfd3752..5f9ee06124c 100644
--- a/nixos/modules/services/system/self-deploy.nix
+++ b/nixos/modules/services/system/self-deploy.nix
@@ -18,7 +18,7 @@ let
     in
     lib.concatStrings (lib.mapAttrsToList toArg args);
 
-  isPathType = x: lib.strings.isCoercibleToString x && builtins.substring 0 1 (toString x) == "/";
+  isPathType = x: lib.types.path.check x;
 
 in
 {
@@ -132,7 +132,7 @@ in
 
       requires = lib.mkIf (!(isPathType cfg.repository)) [ "network-online.target" ];
 
-      environment.GIT_SSH_COMMAND = lib.mkIf (!(isNull cfg.sshKeyFile))
+      environment.GIT_SSH_COMMAND = lib.mkIf (cfg.sshKeyFile != null)
         "${pkgs.openssh}/bin/ssh -i ${lib.escapeShellArg cfg.sshKeyFile}";
 
       restartIfChanged = false;
diff --git a/nixos/modules/services/torrent/flexget.nix b/nixos/modules/services/torrent/flexget.nix
index 2a9ffac18d9..1b971838b32 100644
--- a/nixos/modules/services/torrent/flexget.nix
+++ b/nixos/modules/services/torrent/flexget.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg = config.services.flexget;
-  pkg = pkgs.flexget;
+  pkg = cfg.package;
   ymlFile = pkgs.writeText "flexget.yml" ''
     ${cfg.config}
 
@@ -16,6 +16,8 @@ in {
     services.flexget = {
       enable = mkEnableOption (lib.mdDoc "Run FlexGet Daemon");
 
+      package = mkPackageOptionMD pkgs "flexget" {};
+
       user = mkOption {
         default = "deluge";
         example = "some_user";
diff --git a/nixos/modules/services/torrent/magnetico.nix b/nixos/modules/services/torrent/magnetico.nix
index b813f120511..dc6b4e9aa73 100644
--- a/nixos/modules/services/torrent/magnetico.nix
+++ b/nixos/modules/services/torrent/magnetico.nix
@@ -144,7 +144,7 @@ in {
         interface. If unset no authentication will be required.
 
         The file must contain user names and password hashes in the format
-        `username:hash `, one for each line.  Usernames must
+        `username:hash`, one for each line.  Usernames must
         start with a lowecase ([a-z]) ASCII character, might contain
         non-consecutive underscores except at the end, and consists of
         small-case a-z characters and digits 0-9.
diff --git a/nixos/modules/services/torrent/rtorrent.nix b/nixos/modules/services/torrent/rtorrent.nix
index 627439e1079..64cda7fb675 100644
--- a/nixos/modules/services/torrent/rtorrent.nix
+++ b/nixos/modules/services/torrent/rtorrent.nix
@@ -19,6 +19,15 @@ in {
       '';
     };
 
+    dataPermissions = mkOption {
+      type = types.str;
+      default = "0750";
+      example = "0755";
+      description = lib.mdDoc ''
+        Unix Permissions in octal on the rtorrent directory.
+      '';
+    };
+
     downloadDir = mkOption {
       type = types.str;
       default = "${cfg.dataDir}/download";
@@ -205,7 +214,7 @@ in {
         };
       };
 
-      tmpfiles.rules = [ "d '${cfg.dataDir}' 0750 ${cfg.user} ${cfg.group} -" ];
+      tmpfiles.rules = [ "d '${cfg.dataDir}' ${cfg.dataPermissions} ${cfg.user} ${cfg.group} -" ];
     };
   };
 }
diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix
index 43782338483..752ab91fe63 100644
--- a/nixos/modules/services/torrent/transmission.nix
+++ b/nixos/modules/services/torrent/transmission.nix
@@ -19,8 +19,8 @@ in
   imports = [
     (mkRenamedOptionModule ["services" "transmission" "port"]
                            ["services" "transmission" "settings" "rpc-port"])
-    (mkAliasOptionModule ["services" "transmission" "openFirewall"]
-                         ["services" "transmission" "openPeerPorts"])
+    (mkAliasOptionModuleMD ["services" "transmission" "openFirewall"]
+                           ["services" "transmission" "openPeerPorts"])
   ];
   options = {
     services.transmission = {
@@ -174,7 +174,7 @@ in
         };
       };
 
-      package = mkPackageOption pkgs "transmission" {};
+      package = mkPackageOptionMD pkgs "transmission" {};
 
       downloadDirPermissions = mkOption {
         type = with types; nullOr str;
diff --git a/nixos/modules/services/video/rtsp-simple-server.nix b/nixos/modules/services/video/mediamtx.nix
index 2dd62edab78..18a9e3d5fe3 100644
--- a/nixos/modules/services/video/rtsp-simple-server.nix
+++ b/nixos/modules/services/video/mediamtx.nix
@@ -3,19 +3,19 @@
 with lib;
 
 let
-  cfg = config.services.rtsp-simple-server;
-  package = pkgs.rtsp-simple-server;
+  cfg = config.services.mediamtx;
+  package = pkgs.mediamtx;
   format = pkgs.formats.yaml {};
 in
 {
   options = {
-    services.rtsp-simple-server = {
-      enable = mkEnableOption (lib.mdDoc "RTSP Simple Server");
+    services.mediamtx = {
+      enable = mkEnableOption (lib.mdDoc "MediaMTX");
 
       settings = mkOption {
         description = lib.mdDoc ''
-          Settings for rtsp-simple-server.
-          Read more at <https://github.com/aler9/rtsp-simple-server/blob/main/rtsp-simple-server.yml>
+          Settings for MediaMTX.
+          Read more at <https://github.com/aler9/mediamtx/blob/main/mediamtx.yml>
         '';
         type = format.type;
 
@@ -25,7 +25,7 @@ in
             "stdout"
           ];
           # we set this so when the user uses it, it just works (see LogsDirectory below). but it's not used by default.
-          logFile = "/var/log/rtsp-simple-server/rtsp-simple-server.log";
+          logFile = "/var/log/mediamtx/mediamtx.log";
         };
 
         example = {
@@ -40,20 +40,20 @@ in
 
       env = mkOption {
         type = with types; attrsOf anything;
-        description = lib.mdDoc "Extra environment variables for RTSP Simple Server";
+        description = lib.mdDoc "Extra environment variables for MediaMTX";
         default = {};
         example = {
-          RTSP_CONFKEY = "mykey";
+          MTX_CONFKEY = "mykey";
         };
       };
     };
   };
 
   config = mkIf (cfg.enable) {
-    # NOTE: rtsp-simple-server watches this file and automatically reloads if it changes
-    environment.etc."rtsp-simple-server.yaml".source = format.generate "rtsp-simple-server.yaml" cfg.settings;
+    # NOTE: mediamtx watches this file and automatically reloads if it changes
+    environment.etc."mediamtx.yaml".source = format.generate "mediamtx.yaml" cfg.settings;
 
-    systemd.services.rtsp-simple-server = {
+    systemd.services.mediamtx = {
       environment = cfg.env;
 
       after = [ "network.target" ];
@@ -65,15 +65,15 @@ in
 
       serviceConfig = {
         DynamicUser = true;
-        User = "rtsp-simple-server";
-        Group = "rtsp-simple-server";
+        User = "mediamtx";
+        Group = "mediamtx";
 
-        LogsDirectory = "rtsp-simple-server";
+        LogsDirectory = "mediamtx";
 
         # user likely may want to stream cameras, can't hurt to add video group
         SupplementaryGroups = "video";
 
-        ExecStart = "${package}/bin/rtsp-simple-server /etc/rtsp-simple-server.yaml";
+        ExecStart = "${package}/bin/mediamtx /etc/mediamtx.yaml";
       };
     };
   };
diff --git a/nixos/modules/services/video/unifi-video.nix b/nixos/modules/services/video/unifi-video.nix
index 93d4d5060b7..78dc1908631 100644
--- a/nixos/modules/services/video/unifi-video.nix
+++ b/nixos/modules/services/video/unifi-video.nix
@@ -123,7 +123,7 @@ in
 
     mongodbPackage = mkOption {
       type = types.package;
-      default = pkgs.mongodb-4_0;
+      default = pkgs.mongodb-4_2;
       defaultText = literalExpression "pkgs.mongodb";
       description = lib.mdDoc ''
         The mongodb package to use.
diff --git a/nixos/modules/services/video/v4l2-relayd.nix b/nixos/modules/services/video/v4l2-relayd.nix
new file mode 100644
index 00000000000..2a9dbe00158
--- /dev/null
+++ b/nixos/modules/services/video/v4l2-relayd.nix
@@ -0,0 +1,199 @@
+{ config, lib, pkgs, utils, ... }:
+let
+
+  inherit (lib) attrValues concatStringsSep filterAttrs length listToAttrs literalExpression
+    makeSearchPathOutput mkEnableOption mkIf mkOption nameValuePair optionals types;
+  inherit (utils) escapeSystemdPath;
+
+  cfg = config.services.v4l2-relayd;
+
+  kernelPackages = config.boot.kernelPackages;
+
+  gst = (with pkgs.gst_all_1; [
+    gst-plugins-bad
+    gst-plugins-base
+    gst-plugins-good
+    gstreamer.out
+  ]);
+
+  instanceOpts = { name, ... }: {
+    options = {
+      enable = mkEnableOption (lib.mdDoc "this v4l2-relayd instance");
+
+      name = mkOption {
+        type = types.str;
+        default = name;
+        description = lib.mdDoc ''
+          The name of the instance.
+        '';
+      };
+
+      cardLabel = mkOption {
+        type = types.str;
+        description = lib.mdDoc ''
+          The name the camera will show up as.
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = with types; listOf package;
+        default = [ ];
+        description = lib.mdDoc ''
+          Extra packages to add to {env}`GST_PLUGIN_PATH` for the instance.
+        '';
+      };
+
+      input = {
+        pipeline = mkOption {
+          type = types.str;
+          description = lib.mdDoc ''
+            The gstreamer-pipeline to use for the input-stream.
+          '';
+        };
+
+        format = mkOption {
+          type = types.str;
+          default = "YUY2";
+          description = lib.mdDoc ''
+            The video-format to read from input-stream.
+          '';
+        };
+
+        width = mkOption {
+          type = types.ints.positive;
+          default = 1280;
+          description = lib.mdDoc ''
+            The width to read from input-stream.
+          '';
+        };
+
+        height = mkOption {
+          type = types.ints.positive;
+          default = 720;
+          description = lib.mdDoc ''
+            The height to read from input-stream.
+          '';
+        };
+
+        framerate = mkOption {
+          type = types.ints.positive;
+          default = 30;
+          description = lib.mdDoc ''
+            The framerate to read from input-stream.
+          '';
+        };
+      };
+
+      output = {
+        format = mkOption {
+          type = types.str;
+          default = "YUY2";
+          description = lib.mdDoc ''
+            The video-format to write to output-stream.
+          '';
+        };
+      };
+
+    };
+  };
+
+in
+{
+
+  options.services.v4l2-relayd = {
+
+    instances = mkOption {
+      type = with types; attrsOf (submodule instanceOpts);
+      default = { };
+      example = literalExpression ''
+        {
+          example = {
+            cardLabel = "Example card";
+            input.pipeline = "videotestsrc";
+          };
+        }
+      '';
+      description = lib.mdDoc ''
+        v4l2-relayd instances to be created.
+      '';
+    };
+
+  };
+
+  config =
+    let
+
+      mkInstanceService = instance: {
+        description = "Streaming relay for v4l2loopback using GStreamer";
+
+        after = [ "modprobe@v4l2loopback.service" "systemd-logind.service" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          Type = "simple";
+          Restart = "always";
+          PrivateNetwork = true;
+          PrivateTmp = true;
+          LimitNPROC = 1;
+        };
+
+        environment = {
+          GST_PLUGIN_PATH = makeSearchPathOutput "lib" "lib/gstreamer-1.0" (gst ++ instance.extraPackages);
+          V4L2_DEVICE_FILE = "/run/v4l2-relayd-${instance.name}/device";
+        };
+
+        script =
+          let
+            appsrcOptions = concatStringsSep "," [
+              "caps=video/x-raw"
+              "format=${instance.input.format}"
+              "width=${toString instance.input.width}"
+              "height=${toString instance.input.height}"
+              "framerate=${toString instance.input.framerate}/1"
+            ];
+
+            outputPipeline = [
+              "appsrc name=appsrc ${appsrcOptions}"
+              "videoconvert"
+            ] ++ optionals (instance.input.format != instance.output.format) [
+              "video/x-raw,format=${instance.output.format}"
+              "queue"
+            ] ++ [ "v4l2sink name=v4l2sink device=$(cat $V4L2_DEVICE_FILE)" ];
+          in
+          ''
+            exec ${pkgs.v4l2-relayd}/bin/v4l2-relayd -i "${instance.input.pipeline}" -o "${concatStringsSep " ! " outputPipeline}"
+          '';
+
+        preStart = ''
+          mkdir -p $(dirname $V4L2_DEVICE_FILE)
+          ${kernelPackages.v4l2loopback.bin}/bin/v4l2loopback-ctl add -x 1 -n "${instance.cardLabel}" > $V4L2_DEVICE_FILE
+        '';
+
+        postStop = ''
+          ${kernelPackages.v4l2loopback.bin}/bin/v4l2loopback-ctl delete $(cat $V4L2_DEVICE_FILE)
+          rm -rf $(dirname $V4L2_DEVICE_FILE)
+        '';
+      };
+
+      mkInstanceServices = instances: listToAttrs (map
+        (instance:
+          nameValuePair "v4l2-relayd-${escapeSystemdPath instance.name}" (mkInstanceService instance)
+        )
+        instances);
+
+      enabledInstances = attrValues (filterAttrs (n: v: v.enable) cfg.instances);
+
+    in
+    {
+
+      boot = mkIf ((length enabledInstances) > 0) {
+        extraModulePackages = [ kernelPackages.v4l2loopback ];
+        kernelModules = [ "v4l2loopback" ];
+      };
+
+      systemd.services = mkInstanceServices enabledInstances;
+
+    };
+
+  meta.maintainers = with lib.maintainers; [ betaboon ];
+}
diff --git a/nixos/modules/services/web-apps/akkoma.md b/nixos/modules/services/web-apps/akkoma.md
new file mode 100644
index 00000000000..83dd1a8b35f
--- /dev/null
+++ b/nixos/modules/services/web-apps/akkoma.md
@@ -0,0 +1,332 @@
+# Akkoma {#module-services-akkoma}
+
+[Akkoma](https://akkoma.dev/) is a lightweight ActivityPub microblogging server forked from Pleroma.
+
+## Service configuration {#modules-services-akkoma-service-configuration}
+
+The Elixir configuration file required by Akkoma is generated automatically from
+[{option}`services.akkoma.config`](options.html#opt-services.akkoma.config). Secrets must be
+included from external files outside of the Nix store by setting the configuration option to
+an attribute set containing the attribute {option}`_secret` – a string pointing to the file
+containing the actual value of the option.
+
+For the mandatory configuration settings these secrets will be generated automatically if the
+referenced file does not exist during startup, unless disabled through
+[{option}`services.akkoma.initSecrets`](options.html#opt-services.akkoma.initSecrets).
+
+The following configuration binds Akkoma to the Unix socket `/run/akkoma/socket`, expecting to
+be run behind a HTTP proxy on `fediverse.example.com`.
+
+
+```nix
+services.akkoma.enable = true;
+services.akkoma.config = {
+  ":pleroma" = {
+    ":instance" = {
+      name = "My Akkoma instance";
+      description = "More detailed description";
+      email = "admin@example.com";
+      registration_open = false;
+    };
+
+    "Pleroma.Web.Endpoint" = {
+      url.host = "fediverse.example.com";
+    };
+  };
+};
+```
+
+Please refer to the [configuration cheat sheet](https://docs.akkoma.dev/stable/configuration/cheatsheet/)
+for additional configuration options.
+
+## User management {#modules-services-akkoma-user-management}
+
+After the Akkoma service is running, the administration utility can be used to
+[manage users](https://docs.akkoma.dev/stable/administration/CLI_tasks/user/). In particular an
+administrative user can be created with
+
+```ShellSession
+$ pleroma_ctl user new <nickname> <email> --admin --moderator --password <password>
+```
+
+## Proxy configuration {#modules-services-akkoma-proxy-configuration}
+
+Although it is possible to expose Akkoma directly, it is common practice to operate it behind an
+HTTP reverse proxy such as nginx.
+
+```nix
+services.akkoma.nginx = {
+  enableACME = true;
+  forceSSL = true;
+};
+
+services.nginx = {
+  enable = true;
+
+  clientMaxBodySize = "16m";
+  recommendedTlsSettings = true;
+  recommendedOptimisation = true;
+  recommendedGzipSettings = true;
+};
+```
+
+Please refer to [](#module-security-acme) for details on how to provision an SSL/TLS certificate.
+
+### Media proxy {#modules-services-akkoma-media-proxy}
+
+Without the media proxy function, Akkoma does not store any remote media like pictures or video
+locally, and clients have to fetch them directly from the source server.
+
+```nix
+# Enable nginx slice module distributed with Tengine
+services.nginx.package = pkgs.tengine;
+
+# Enable media proxy
+services.akkoma.config.":pleroma".":media_proxy" = {
+  enabled = true;
+  proxy_opts.redirect_on_failure = true;
+};
+
+# Adjust the persistent cache size as needed:
+#  Assuming an average object size of 128 KiB, around 1 MiB
+#  of memory is required for the key zone per GiB of cache.
+# Ensure that the cache directory exists and is writable by nginx.
+services.nginx.commonHttpConfig = ''
+  proxy_cache_path /var/cache/nginx/cache/akkoma-media-cache
+    levels= keys_zone=akkoma_media_cache:16m max_size=16g
+    inactive=1y use_temp_path=off;
+'';
+
+services.akkoma.nginx = {
+  locations."/proxy" = {
+    proxyPass = "http://unix:/run/akkoma/socket";
+
+    extraConfig = ''
+      proxy_cache akkoma_media_cache;
+
+      # Cache objects in slices of 1 MiB
+      slice 1m;
+      proxy_cache_key $host$uri$is_args$args$slice_range;
+      proxy_set_header Range $slice_range;
+
+      # Decouple proxy and upstream responses
+      proxy_buffering on;
+      proxy_cache_lock on;
+      proxy_ignore_client_abort on;
+
+      # Default cache times for various responses
+      proxy_cache_valid 200 1y;
+      proxy_cache_valid 206 301 304 1h;
+
+      # Allow serving of stale items
+      proxy_cache_use_stale error timeout invalid_header updating;
+    '';
+  };
+};
+```
+
+#### Prefetch remote media {#modules-services-akkoma-prefetch-remote-media}
+
+The following example enables the `MediaProxyWarmingPolicy` MRF policy which automatically
+fetches all media associated with a post through the media proxy, as soon as the post is
+received by the instance.
+
+```nix
+services.akkoma.config.":pleroma".":mrf".policies =
+  map (pkgs.formats.elixirConf { }).lib.mkRaw [
+    "Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy"
+];
+```
+
+#### Media previews {#modules-services-akkoma-media-previews}
+
+Akkoma can generate previews for media.
+
+```nix
+services.akkoma.config.":pleroma".":media_preview_proxy" = {
+  enabled = true;
+  thumbnail_max_width = 1920;
+  thumbnail_max_height = 1080;
+};
+```
+
+## Frontend management {#modules-services-akkoma-frontend-management}
+
+Akkoma will be deployed with the `akkoma-fe` and `admin-fe` frontends by default. These can be
+modified by setting
+[{option}`services.akkoma.frontends`](options.html#opt-services.akkoma.frontends).
+
+The following example overrides the primary frontend’s default configuration using a custom
+derivation.
+
+```nix
+services.akkoma.frontends.primary.package = pkgs.runCommand "akkoma-fe" {
+  config = builtins.toJSON {
+    expertLevel = 1;
+    collapseMessageWithSubject = false;
+    stopGifs = false;
+    replyVisibility = "following";
+    webPushHideIfCW = true;
+    hideScopeNotice = true;
+    renderMisskeyMarkdown = false;
+    hideSiteFavicon = true;
+    postContentType = "text/markdown";
+    showNavShortcuts = false;
+  };
+  nativeBuildInputs = with pkgs; [ jq xorg.lndir ];
+  passAsFile = [ "config" ];
+} ''
+  mkdir $out
+  lndir ${pkgs.akkoma-frontends.akkoma-fe} $out
+
+  rm $out/static/config.json
+  jq -s add ${pkgs.akkoma-frontends.akkoma-fe}/static/config.json ${config} \
+    >$out/static/config.json
+'';
+```
+
+## Federation policies {#modules-services-akkoma-federation-policies}
+
+Akkoma comes with a number of modules to police federation with other ActivityPub instances.
+The most valuable for typical users is the
+[`:mrf_simple`](https://docs.akkoma.dev/stable/configuration/cheatsheet/#mrf_simple) module
+which allows limiting federation based on instance hostnames.
+
+This configuration snippet provides an example on how these can be used. Choosing an adequate
+federation policy is not trivial and entails finding a balance between connectivity to the rest
+of the fediverse and providing a pleasant experience to the users of an instance.
+
+
+```nix
+services.akkoma.config.":pleroma" = with (pkgs.formats.elixirConf { }).lib; {
+  ":mrf".policies = map mkRaw [
+    "Pleroma.Web.ActivityPub.MRF.SimplePolicy"
+  ];
+
+  ":mrf_simple" = {
+    # Tag all media as sensitive
+    media_nsfw = mkMap {
+      "nsfw.weird.kinky" = "Untagged NSFW content";
+    };
+
+    # Reject all activities except deletes
+    reject = mkMap {
+      "kiwifarms.cc" = "Persistent harassment of users, no moderation";
+    };
+
+    # Force posts to be visible by followers only
+    followers_only = mkMap {
+      "beta.birdsite.live" = "Avoid polluting timelines with Twitter posts";
+    };
+  };
+};
+```
+
+## Upload filters {#modules-services-akkoma-upload-filters}
+
+This example strips GPS and location metadata from uploads, deduplicates them and anonymises the
+the file name.
+
+```nix
+services.akkoma.config.":pleroma"."Pleroma.Upload".filters =
+  map (pkgs.formats.elixirConf { }).lib.mkRaw [
+    "Pleroma.Upload.Filter.Exiftool"
+    "Pleroma.Upload.Filter.Dedupe"
+    "Pleroma.Upload.Filter.AnonymizeFilename"
+  ];
+```
+
+## Migration from Pleroma {#modules-services-akkoma-migration-pleroma}
+
+Pleroma instances can be migrated to Akkoma either by copying the database and upload data or by
+pointing Akkoma to the existing data. The necessary database migrations are run automatically
+during startup of the service.
+
+The configuration has to be copy‐edited manually.
+
+Depending on the size of the database, the initial migration may take a long time and exceed the
+startup timeout of the system manager. To work around this issue one may adjust the startup timeout
+{option}`systemd.services.akkoma.serviceConfig.TimeoutStartSec` or simply run the migrations
+manually:
+
+```ShellSession
+pleroma_ctl migrate
+```
+
+### Copying data {#modules-services-akkoma-migration-pleroma-copy}
+
+Copying the Pleroma data instead of re‐using it in place may permit easier reversion to Pleroma,
+but allows the two data sets to diverge.
+
+First disable Pleroma and then copy its database and upload data:
+
+```ShellSession
+# Create a copy of the database
+nix-shell -p postgresql --run 'createdb -T pleroma akkoma'
+
+# Copy upload data
+mkdir /var/lib/akkoma
+cp -R --reflink=auto /var/lib/pleroma/uploads /var/lib/akkoma/
+```
+
+After the data has been copied, enable the Akkoma service and verify that the migration has been
+successful. If no longer required, the original data may then be deleted:
+
+```ShellSession
+# Delete original database
+nix-shell -p postgresql --run 'dropdb pleroma'
+
+# Delete original Pleroma state
+rm -r /var/lib/pleroma
+```
+
+### Re‐using data {#modules-services-akkoma-migration-pleroma-reuse}
+
+To re‐use the Pleroma data in place, disable Pleroma and enable Akkoma, pointing it to the
+Pleroma database and upload directory.
+
+```nix
+# Adjust these settings according to the database name and upload directory path used by Pleroma
+services.akkoma.config.":pleroma"."Pleroma.Repo".database = "pleroma";
+services.akkoma.config.":pleroma".":instance".upload_dir = "/var/lib/pleroma/uploads";
+```
+
+Please keep in mind that after the Akkoma service has been started, any migrations applied by
+Akkoma have to be rolled back before the database can be used again with Pleroma. This can be
+achieved through `pleroma_ctl ecto.rollback`. Refer to the
+[Ecto SQL documentation](https://hexdocs.pm/ecto_sql/Mix.Tasks.Ecto.Rollback.html) for
+details.
+
+## Advanced deployment options {#modules-services-akkoma-advanced-deployment}
+
+### Confinement {#modules-services-akkoma-confinement}
+
+The Akkoma systemd service may be confined to a chroot with
+
+```nix
+services.systemd.akkoma.confinement.enable = true;
+```
+
+Confinement of services is not generally supported in NixOS and therefore disabled by default.
+Depending on the Akkoma configuration, the default confinement settings may be insufficient and
+lead to subtle errors at run time, requiring adjustment:
+
+Use
+[{option}`services.systemd.akkoma.confinement.packages`](options.html#opt-systemd.services._name_.confinement.packages)
+to make packages available in the chroot.
+
+{option}`services.systemd.akkoma.serviceConfig.BindPaths` and
+{option}`services.systemd.akkoma.serviceConfig.BindReadOnlyPaths` permit access to outside paths
+through bind mounts. Refer to
+[`BindPaths=`](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#BindPaths=)
+of {manpage}`systemd.exec(5)` for details.
+
+### Distributed deployment {#modules-services-akkoma-distributed-deployment}
+
+Being an Elixir application, Akkoma can be deployed in a distributed fashion.
+
+This requires setting
+[{option}`services.akkoma.dist.address`](options.html#opt-services.akkoma.dist.address) and
+[{option}`services.akkoma.dist.cookie`](options.html#opt-services.akkoma.dist.cookie). The
+specifics depend strongly on the deployment environment. For more information please check the
+relevant [Erlang documentation](https://www.erlang.org/doc/reference_manual/distributed.html).
diff --git a/nixos/modules/services/web-apps/akkoma.nix b/nixos/modules/services/web-apps/akkoma.nix
new file mode 100644
index 00000000000..8d177525861
--- /dev/null
+++ b/nixos/modules/services/web-apps/akkoma.nix
@@ -0,0 +1,1086 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.akkoma;
+  ex = cfg.config;
+  db = ex.":pleroma"."Pleroma.Repo";
+  web = ex.":pleroma"."Pleroma.Web.Endpoint";
+
+  isConfined = config.systemd.services.akkoma.confinement.enable;
+  hasSmtp = (attrByPath [ ":pleroma" "Pleroma.Emails.Mailer" "adapter" "value" ] null ex) == "Swoosh.Adapters.SMTP";
+
+  isAbsolutePath = v: isString v && substring 0 1 v == "/";
+  isSecret = v: isAttrs v && v ? _secret && isAbsolutePath v._secret;
+
+  absolutePath = with types; mkOptionType {
+    name = "absolutePath";
+    description = "absolute path";
+    descriptionClass = "noun";
+    check = isAbsolutePath;
+    inherit (str) merge;
+  };
+
+  secret = mkOptionType {
+    name = "secret";
+    description = "secret value";
+    descriptionClass = "noun";
+    check = isSecret;
+    nestedTypes = {
+      _secret = absolutePath;
+    };
+  };
+
+  ipAddress = with types; mkOptionType {
+    name = "ipAddress";
+    description = "IPv4 or IPv6 address";
+    descriptionClass = "conjunction";
+    check = x: str.check x && builtins.match "[.0-9:A-Fa-f]+" x != null;
+    inherit (str) merge;
+  };
+
+  elixirValue = let
+    elixirValue' = with types;
+      nullOr (oneOf [ bool int float str (attrsOf elixirValue') (listOf elixirValue') ]) // {
+        description = "Elixir value";
+      };
+  in elixirValue';
+
+  frontend = {
+    options = {
+      package = mkOption {
+        type = types.package;
+        description = mdDoc "Akkoma frontend package.";
+        example = literalExpression "pkgs.akkoma-frontends.akkoma-fe";
+      };
+
+      name = mkOption {
+        type = types.nonEmptyStr;
+        description = mdDoc "Akkoma frontend name.";
+        example = "akkoma-fe";
+      };
+
+      ref = mkOption {
+        type = types.nonEmptyStr;
+        description = mdDoc "Akkoma frontend reference.";
+        example = "stable";
+      };
+    };
+  };
+
+  sha256 = builtins.hashString "sha256";
+
+  replaceSec = let
+    replaceSec' = { }@args: v:
+      if isAttrs v
+        then if v ? _secret
+          then if isAbsolutePath v._secret
+            then sha256 v._secret
+            else abort "Invalid secret path (_secret = ${v._secret})"
+          else mapAttrs (_: val: replaceSec' args val) v
+        else if isList v
+          then map (replaceSec' args) v
+          else v;
+    in replaceSec' { };
+
+  # Erlang/Elixir uses a somewhat special format for IP addresses
+  erlAddr = addr: fileContents
+    (pkgs.runCommand addr {
+      nativeBuildInputs = with pkgs; [ elixir ];
+      code = ''
+        case :inet.parse_address('${addr}') do
+          {:ok, addr} -> IO.inspect addr
+          {:error, _} -> System.halt(65)
+        end
+      '';
+      passAsFile = [ "code" ];
+    } ''elixir "$codePath" >"$out"'');
+
+  format = pkgs.formats.elixirConf { };
+  configFile = format.generate "config.exs"
+    (replaceSec
+      (attrsets.updateManyAttrsByPath [{
+        path = [ ":pleroma" "Pleroma.Web.Endpoint" "http" "ip" ];
+        update = addr:
+          if isAbsolutePath addr
+            then format.lib.mkTuple
+              [ (format.lib.mkAtom ":local") addr ]
+            else format.lib.mkRaw (erlAddr addr);
+      }] cfg.config));
+
+  writeShell = { name, text, runtimeInputs ? [ ] }:
+    pkgs.writeShellApplication { inherit name text runtimeInputs; } + "/bin/${name}";
+
+  genScript = writeShell {
+    name = "akkoma-gen-cookie";
+    runtimeInputs = with pkgs; [ coreutils util-linux ];
+    text = ''
+      install -m 0400 \
+        -o ${escapeShellArg cfg.user } \
+        -g ${escapeShellArg cfg.group} \
+        <(hexdump -n 16 -e '"%02x"' /dev/urandom) \
+        "$RUNTIME_DIRECTORY/cookie"
+    '';
+  };
+
+  copyScript = writeShell {
+    name = "akkoma-copy-cookie";
+    runtimeInputs = with pkgs; [ coreutils ];
+    text = ''
+      install -m 0400 \
+        -o ${escapeShellArg cfg.user} \
+        -g ${escapeShellArg cfg.group} \
+        ${escapeShellArg cfg.dist.cookie._secret} \
+        "$RUNTIME_DIRECTORY/cookie"
+    '';
+  };
+
+  secretPaths = catAttrs "_secret" (collect isSecret cfg.config);
+
+  vapidKeygen = pkgs.writeText "vapidKeygen.exs" ''
+    [public_path, private_path] = System.argv()
+    {public_key, private_key} = :crypto.generate_key :ecdh, :prime256v1
+    File.write! public_path, Base.url_encode64(public_key, padding: false)
+    File.write! private_path, Base.url_encode64(private_key, padding: false)
+  '';
+
+  initSecretsScript = writeShell {
+    name = "akkoma-init-secrets";
+    runtimeInputs = with pkgs; [ coreutils elixir ];
+    text = let
+      key-base = web.secret_key_base;
+      jwt-signer = ex.":joken".":default_signer";
+      signing-salt = web.signing_salt;
+      liveview-salt = web.live_view.signing_salt;
+      vapid-private = ex.":web_push_encryption".":vapid_details".private_key;
+      vapid-public = ex.":web_push_encryption".":vapid_details".public_key;
+    in ''
+      secret() {
+        # Generate default secret if non‐existent
+        test -e "$2" || install -D -m 0600 <(tr -dc 'A-Za-z-._~' </dev/urandom | head -c "$1") "$2"
+        if [ "$(stat --dereference --format='%s' "$2")" -lt "$1" ]; then
+          echo "Secret '$2' is smaller than minimum size of $1 bytes." >&2
+          exit 65
+        fi
+      }
+
+      secret 64 ${escapeShellArg key-base._secret}
+      secret 64 ${escapeShellArg jwt-signer._secret}
+      secret 8 ${escapeShellArg signing-salt._secret}
+      secret 8 ${escapeShellArg liveview-salt._secret}
+
+      ${optionalString (isSecret vapid-public) ''
+        { test -e ${escapeShellArg vapid-private._secret} && \
+          test -e ${escapeShellArg vapid-public._secret}; } || \
+            elixir ${escapeShellArgs [ vapidKeygen vapid-public._secret vapid-private._secret ]}
+      ''}
+    '';
+  };
+
+  configScript = writeShell {
+    name = "akkoma-config";
+    runtimeInputs = with pkgs; [ coreutils replace-secret ];
+    text = ''
+      cd "$RUNTIME_DIRECTORY"
+      tmp="$(mktemp config.exs.XXXXXXXXXX)"
+      trap 'rm -f "$tmp"' EXIT TERM
+
+      cat ${escapeShellArg configFile} >"$tmp"
+      ${concatMapStrings (file: ''
+        replace-secret ${escapeShellArgs [ (sha256 file) file ]} "$tmp"
+      '') secretPaths}
+
+      chown ${escapeShellArg cfg.user}:${escapeShellArg cfg.group} "$tmp"
+      chmod 0400 "$tmp"
+      mv -f "$tmp" config.exs
+    '';
+  };
+
+  pgpass = let
+    esc = escape [ ":" ''\'' ];
+  in if (cfg.initDb.password != null)
+    then pkgs.writeText "pgpass.conf" ''
+      *:*:*${esc cfg.initDb.username}:${esc (sha256 cfg.initDb.password._secret)}
+    ''
+    else null;
+
+  escapeSqlId = x: ''"${replaceStrings [ ''"'' ] [ ''""'' ] x}"'';
+  escapeSqlStr = x: "'${replaceStrings [ "'" ] [ "''" ] x}'";
+
+  setupSql = pkgs.writeText "setup.psql" ''
+    \set ON_ERROR_STOP on
+
+    ALTER ROLE ${escapeSqlId db.username}
+      LOGIN PASSWORD ${if db ? password
+        then "${escapeSqlStr (sha256 db.password._secret)}"
+        else "NULL"};
+
+    ALTER DATABASE ${escapeSqlId db.database}
+      OWNER TO ${escapeSqlId db.username};
+
+    \connect ${escapeSqlId db.database}
+    CREATE EXTENSION IF NOT EXISTS citext;
+    CREATE EXTENSION IF NOT EXISTS pg_trgm;
+    CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+  '';
+
+  dbHost = if db ? socket_dir then db.socket_dir
+    else if db ? socket then db.socket
+      else if db ? hostname then db.hostname
+        else null;
+
+  initDbScript = writeShell {
+    name = "akkoma-initdb";
+    runtimeInputs = with pkgs; [ coreutils replace-secret config.services.postgresql.package ];
+    text = ''
+      pgpass="$(mktemp -t pgpass-XXXXXXXXXX.conf)"
+      setupSql="$(mktemp -t setup-XXXXXXXXXX.psql)"
+      trap 'rm -f "$pgpass $setupSql"' EXIT TERM
+
+      ${optionalString (dbHost != null) ''
+        export PGHOST=${escapeShellArg dbHost}
+      ''}
+      export PGUSER=${escapeShellArg cfg.initDb.username}
+      ${optionalString (pgpass != null) ''
+        cat ${escapeShellArg pgpass} >"$pgpass"
+        replace-secret ${escapeShellArgs [
+          (sha256 cfg.initDb.password._secret) cfg.initDb.password._secret ]} "$pgpass"
+        export PGPASSFILE="$pgpass"
+      ''}
+
+      cat ${escapeShellArg setupSql} >"$setupSql"
+      ${optionalString (db ? password) ''
+        replace-secret ${escapeShellArgs [
+         (sha256 db.password._secret) db.password._secret ]} "$setupSql"
+      ''}
+
+      # Create role if non‐existent
+      psql -tAc "SELECT 1 FROM pg_roles
+        WHERE rolname = "${escapeShellArg (escapeSqlStr db.username)} | grep -F -q 1 || \
+        psql -tAc "CREATE ROLE "${escapeShellArg (escapeSqlId db.username)}
+
+      # Create database if non‐existent
+      psql -tAc "SELECT 1 FROM pg_database
+        WHERE datname = "${escapeShellArg (escapeSqlStr db.database)} | grep -F -q 1 || \
+        psql -tAc "CREATE DATABASE "${escapeShellArg (escapeSqlId db.database)}"
+          OWNER "${escapeShellArg (escapeSqlId db.username)}"
+          TEMPLATE template0
+          ENCODING 'utf8'
+          LOCALE 'C'"
+
+      psql -f "$setupSql"
+    '';
+  };
+
+  envWrapper = let
+    script = writeShell {
+      name = "akkoma-env";
+      text = ''
+        cd "${cfg.package}"
+
+        RUNTIME_DIRECTORY="''${RUNTIME_DIRECTORY:-/run/akkoma}"
+        AKKOMA_CONFIG_PATH="$RUNTIME_DIRECTORY/config.exs" \
+        ERL_EPMD_ADDRESS="${cfg.dist.address}" \
+        ERL_EPMD_PORT="${toString cfg.dist.epmdPort}" \
+        ERL_FLAGS="${concatStringsSep " " [
+          "-kernel inet_dist_use_interface '${erlAddr cfg.dist.address}'"
+          "-kernel inet_dist_listen_min ${toString cfg.dist.portMin}"
+          "-kernel inet_dist_listen_max ${toString cfg.dist.portMax}"
+        ]}" \
+        RELEASE_COOKIE="$(<"$RUNTIME_DIRECTORY/cookie")" \
+        RELEASE_NAME="akkoma" \
+          exec "${cfg.package}/bin/$(basename "$0")" "$@"
+      '';
+    };
+  in pkgs.runCommandLocal "akkoma-env" { } ''
+    mkdir -p "$out/bin"
+
+    ln -r -s ${escapeShellArg script} "$out/bin/pleroma"
+    ln -r -s ${escapeShellArg script} "$out/bin/pleroma_ctl"
+  '';
+
+  userWrapper = pkgs.writeShellApplication {
+    name = "pleroma_ctl";
+    text = ''
+      if [ "''${1-}" == "update" ]; then
+        echo "OTP releases are not supported on NixOS." >&2
+        exit 64
+      fi
+
+      exec sudo -u ${escapeShellArg cfg.user} \
+        "${envWrapper}/bin/pleroma_ctl" "$@"
+    '';
+  };
+
+  socketScript = if isAbsolutePath web.http.ip
+    then writeShell {
+      name = "akkoma-socket";
+      runtimeInputs = with pkgs; [ coreutils inotify-tools ];
+      text = ''
+        coproc {
+          inotifywait -q -m -e create ${escapeShellArg (dirOf web.http.ip)}
+        }
+
+        trap 'kill "$COPROC_PID"' EXIT TERM
+
+        until test -S ${escapeShellArg web.http.ip}
+          do read -r -u "''${COPROC[0]}"
+        done
+
+        chmod 0666 ${escapeShellArg web.http.ip}
+      '';
+    }
+    else null;
+
+  staticDir = ex.":pleroma".":instance".static_dir;
+  uploadDir = ex.":pleroma".":instance".upload_dir;
+
+  staticFiles = pkgs.runCommandLocal "akkoma-static" { } ''
+    ${concatStringsSep "\n" (mapAttrsToList (key: val: ''
+      mkdir -p $out/frontends/${escapeShellArg val.name}/
+      ln -s ${escapeShellArg val.package} $out/frontends/${escapeShellArg val.name}/${escapeShellArg val.ref}
+    '') cfg.frontends)}
+
+    ${optionalString (cfg.extraStatic != null)
+      (concatStringsSep "\n" (mapAttrsToList (key: val: ''
+        mkdir -p "$out/$(dirname ${escapeShellArg key})"
+        ln -s ${escapeShellArg val} $out/${escapeShellArg key}
+      '') cfg.extraStatic))}
+  '';
+in {
+  options = {
+    services.akkoma = {
+      enable = mkEnableOption (mdDoc "Akkoma");
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.akkoma;
+        defaultText = literalExpression "pkgs.akkoma";
+        description = mdDoc "Akkoma package to use.";
+      };
+
+      user = mkOption {
+        type = types.nonEmptyStr;
+        default = "akkoma";
+        description = mdDoc "User account under which Akkoma runs.";
+      };
+
+      group = mkOption {
+        type = types.nonEmptyStr;
+        default = "akkoma";
+        description = mdDoc "Group account under which Akkoma runs.";
+      };
+
+      initDb = {
+        enable = mkOption {
+          type = types.bool;
+          default = true;
+          description = mdDoc ''
+            Whether to automatically initialise the database on startup. This will create a
+            database role and database if they do not already exist, and (re)set the role password
+            and the ownership of the database.
+
+            This setting can be used safely even if the database already exists and contains data.
+
+            The database settings are configured through
+            [{option}`config.services.akkoma.config.":pleroma"."Pleroma.Repo"`](#opt-services.akkoma.config.__pleroma_._Pleroma.Repo_).
+
+            If disabled, the database has to be set up manually:
+
+            ```SQL
+            CREATE ROLE akkoma LOGIN;
+
+            CREATE DATABASE akkoma
+              OWNER akkoma
+              TEMPLATE template0
+              ENCODING 'utf8'
+              LOCALE 'C';
+
+            \connect akkoma
+            CREATE EXTENSION IF NOT EXISTS citext;
+            CREATE EXTENSION IF NOT EXISTS pg_trgm;
+            CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+            ```
+          '';
+        };
+
+        username = mkOption {
+          type = types.nonEmptyStr;
+          default = config.services.postgresql.superUser;
+          defaultText = literalExpression "config.services.postgresql.superUser";
+          description = mdDoc ''
+            Name of the database user to initialise the database with.
+
+            This user is required to have the `CREATEROLE` and `CREATEDB` capabilities.
+          '';
+        };
+
+        password = mkOption {
+          type = types.nullOr secret;
+          default = null;
+          description = mdDoc ''
+            Password of the database user to initialise the database with.
+
+            If set to `null`, no password will be used.
+
+            The attribute `_secret` should point to a file containing the secret.
+          '';
+        };
+      };
+
+      initSecrets = mkOption {
+        type = types.bool;
+        default = true;
+        description = mdDoc ''
+          Whether to initialise non‐existent secrets with random values.
+
+          If enabled, appropriate secrets for the following options will be created automatically
+          if the files referenced in the `_secrets` attribute do not exist during startup.
+
+          - {option}`config.":pleroma"."Pleroma.Web.Endpoint".secret_key_base`
+          - {option}`config.":pleroma"."Pleroma.Web.Endpoint".signing_salt`
+          - {option}`config.":pleroma"."Pleroma.Web.Endpoint".live_view.signing_salt`
+          - {option}`config.":web_push_encryption".":vapid_details".private_key`
+          - {option}`config.":web_push_encryption".":vapid_details".public_key`
+          - {option}`config.":joken".":default_signer"`
+        '';
+      };
+
+      installWrapper = mkOption {
+        type = types.bool;
+        default = true;
+        description = mdDoc ''
+          Whether to install a wrapper around `pleroma_ctl` to simplify administration of the
+          Akkoma instance.
+        '';
+      };
+
+      extraPackages = mkOption {
+        type = with types; listOf package;
+        default = with pkgs; [ exiftool ffmpeg_5-headless graphicsmagick-imagemagick-compat ];
+        defaultText = literalExpression "with pkgs; [ exiftool graphicsmagick-imagemagick-compat ffmpeg_5-headless ]";
+        example = literalExpression "with pkgs; [ exiftool imagemagick ffmpeg_5-full ]";
+        description = mdDoc ''
+          List of extra packages to include in the executable search path of the service unit.
+          These are needed by various configurable components such as:
+
+          - ExifTool for the `Pleroma.Upload.Filter.Exiftool` upload filter,
+          - ImageMagick for still image previews in the media proxy as well as for the
+            `Pleroma.Upload.Filters.Mogrify` upload filter, and
+          - ffmpeg for video previews in the media proxy.
+        '';
+      };
+
+      frontends = mkOption {
+        description = mdDoc "Akkoma frontends.";
+        type = with types; attrsOf (submodule frontend);
+        default = {
+          primary = {
+            package = pkgs.akkoma-frontends.akkoma-fe;
+            name = "akkoma-fe";
+            ref = "stable";
+          };
+          admin = {
+            package = pkgs.akkoma-frontends.admin-fe;
+            name = "admin-fe";
+            ref = "stable";
+          };
+        };
+        defaultText = literalExpression ''
+          {
+            primary = {
+              package = pkgs.akkoma-frontends.akkoma-fe;
+              name = "akkoma-fe";
+              ref = "stable";
+            };
+            admin = {
+              package = pkgs.akkoma-frontends.admin-fe;
+              name = "admin-fe";
+              ref = "stable";
+            };
+          }
+        '';
+      };
+
+      extraStatic = mkOption {
+        type = with types; nullOr (attrsOf package);
+        description = mdDoc ''
+          Attribute set of extra packages to add to the static files directory.
+
+          Do not add frontends here. These should be configured through
+          [{option}`services.akkoma.frontends`](#opt-services.akkoma.frontends).
+        '';
+        default = null;
+        example = literalExpression ''
+          {
+            "emoji/blobs.gg" = pkgs.akkoma-emoji.blobs_gg;
+            "static/terms-of-service.html" = pkgs.writeText "terms-of-service.html" '''
+              …
+            ''';
+            "favicon.png" = let
+              rev = "697a8211b0f427a921e7935a35d14bb3e32d0a2c";
+            in pkgs.stdenvNoCC.mkDerivation {
+              name = "favicon.png";
+
+              src = pkgs.fetchurl {
+                url = "https://raw.githubusercontent.com/TilCreator/NixOwO/''${rev}/NixOwO_plain.svg";
+                hash = "sha256-tWhHMfJ3Od58N9H5yOKPMfM56hYWSOnr/TGCBi8bo9E=";
+              };
+
+              nativeBuildInputs = with pkgs; [ librsvg ];
+
+              dontUnpack = true;
+              installPhase = '''
+                rsvg-convert -o $out -w 96 -h 96 $src
+              ''';
+            };
+          }
+        '';
+      };
+
+      dist = {
+        address = mkOption {
+          type = ipAddress;
+          default = "127.0.0.1";
+          description = mdDoc ''
+            Listen address for Erlang distribution protocol and Port Mapper Daemon (epmd).
+          '';
+        };
+
+        epmdPort = mkOption {
+          type = types.port;
+          default = 4369;
+          description = mdDoc "TCP port to bind Erlang Port Mapper Daemon to.";
+        };
+
+        portMin = mkOption {
+          type = types.port;
+          default = 49152;
+          description = mdDoc "Lower bound for Erlang distribution protocol TCP port.";
+        };
+
+        portMax = mkOption {
+          type = types.port;
+          default = 65535;
+          description = mdDoc "Upper bound for Erlang distribution protocol TCP port.";
+        };
+
+        cookie = mkOption {
+          type = types.nullOr secret;
+          default = null;
+          example = { _secret = "/var/lib/secrets/akkoma/releaseCookie"; };
+          description = mdDoc ''
+            Erlang release cookie.
+
+            If set to `null`, a temporary random cookie will be generated.
+          '';
+        };
+      };
+
+      config = mkOption {
+        description = mdDoc ''
+          Configuration for Akkoma. The attributes are serialised to Elixir DSL.
+
+          Refer to <https://docs.akkoma.dev/stable/configuration/cheatsheet/> for
+          configuration options.
+
+          Settings containing secret data should be set to an attribute set containing the
+          attribute `_secret` - a string pointing to a file containing the value the option
+          should be set to.
+        '';
+        type = types.submodule {
+          freeformType = format.type;
+          options = {
+            ":pleroma" = {
+              ":instance" = {
+                name = mkOption {
+                  type = types.nonEmptyStr;
+                  description = mdDoc "Instance name.";
+                };
+
+                email = mkOption {
+                  type = types.nonEmptyStr;
+                  description = mdDoc "Instance administrator email.";
+                };
+
+                description = mkOption {
+                  type = types.nonEmptyStr;
+                  description = mdDoc "Instance description.";
+                };
+
+                static_dir = mkOption {
+                  type = types.path;
+                  default = toString staticFiles;
+                  defaultText = literalMD ''
+                    Derivation gathering the following paths into a directory:
+
+                    - [{option}`services.akkoma.frontends`](#opt-services.akkoma.frontends)
+                    - [{option}`services.akkoma.extraStatic`](#opt-services.akkoma.extraStatic)
+                  '';
+                  description = mdDoc ''
+                    Directory of static files.
+
+                    This directory can be built using a derivation, or it can be managed as mutable
+                    state by setting the option to an absolute path.
+                  '';
+                };
+
+                upload_dir = mkOption {
+                  type = absolutePath;
+                  default = "/var/lib/akkoma/uploads";
+                  description = mdDoc ''
+                    Directory where Akkoma will put uploaded files.
+                  '';
+                };
+              };
+
+              "Pleroma.Repo" = mkOption {
+                type = elixirValue;
+                default = {
+                  adapter = format.lib.mkRaw "Ecto.Adapters.Postgres";
+                  socket_dir = "/run/postgresql";
+                  username = cfg.user;
+                  database = "akkoma";
+                };
+                defaultText = literalExpression ''
+                  {
+                    adapter = (pkgs.formats.elixirConf { }).lib.mkRaw "Ecto.Adapters.Postgres";
+                    socket_dir = "/run/postgresql";
+                    username = config.services.akkoma.user;
+                    database = "akkoma";
+                  }
+                '';
+                description = mdDoc ''
+                  Database configuration.
+
+                  Refer to
+                  <https://hexdocs.pm/ecto_sql/Ecto.Adapters.Postgres.html#module-connection-options>
+                  for options.
+                '';
+              };
+
+              "Pleroma.Web.Endpoint" = {
+                url = {
+                  host = mkOption {
+                    type = types.nonEmptyStr;
+                    default = config.networking.fqdn;
+                    defaultText = literalExpression "config.networking.fqdn";
+                    description = mdDoc "Domain name of the instance.";
+                  };
+
+                  scheme = mkOption {
+                    type = types.nonEmptyStr;
+                    default = "https";
+                    description = mdDoc "URL scheme.";
+                  };
+
+                  port = mkOption {
+                    type = types.port;
+                    default = 443;
+                    description = mdDoc "External port number.";
+                  };
+                };
+
+                http = {
+                  ip = mkOption {
+                    type = types.either absolutePath ipAddress;
+                    default = "/run/akkoma/socket";
+                    example = "::1";
+                    description = mdDoc ''
+                      Listener IP address or Unix socket path.
+
+                      The value is automatically converted to Elixir’s internal address
+                      representation during serialisation.
+                    '';
+                  };
+
+                  port = mkOption {
+                    type = types.port;
+                    default = if isAbsolutePath web.http.ip then 0 else 4000;
+                    defaultText = literalExpression ''
+                      if isAbsolutePath config.services.akkoma.config.:pleroma"."Pleroma.Web.Endpoint".http.ip
+                        then 0
+                        else 4000;
+                    '';
+                    description = mdDoc ''
+                      Listener port number.
+
+                      Must be 0 if using a Unix socket.
+                    '';
+                  };
+                };
+
+                secret_key_base = mkOption {
+                  type = secret;
+                  default = { _secret = "/var/lib/secrets/akkoma/key-base"; };
+                  description = mdDoc ''
+                    Secret key used as a base to generate further secrets for encrypting and
+                    signing data.
+
+                    The attribute `_secret` should point to a file containing the secret.
+
+                    This key can generated can be generated as follows:
+
+                    ```ShellSession
+                    $ tr -dc 'A-Za-z-._~' </dev/urandom | head -c 64
+                    ```
+                  '';
+                };
+
+                live_view = {
+                  signing_salt = mkOption {
+                    type = secret;
+                    default = { _secret = "/var/lib/secrets/akkoma/liveview-salt"; };
+                    description = mdDoc ''
+                      LiveView signing salt.
+
+                      The attribute `_secret` should point to a file containing the secret.
+
+                      This salt can be generated as follows:
+
+                      ```ShellSession
+                      $ tr -dc 'A-Za-z0-9-._~' </dev/urandom | head -c 8
+                      ```
+                    '';
+                  };
+                };
+
+                signing_salt = mkOption {
+                  type = secret;
+                  default = { _secret = "/var/lib/secrets/akkoma/signing-salt"; };
+                  description = mdDoc ''
+                    Signing salt.
+
+                    The attribute `_secret` should point to a file containing the secret.
+
+                    This salt can be generated as follows:
+
+                    ```ShellSession
+                    $ tr -dc 'A-Za-z0-9-._~' </dev/urandom | head -c 8
+                    ```
+                  '';
+                };
+              };
+
+              ":frontends" = mkOption {
+                type = elixirValue;
+                default = mapAttrs
+                  (key: val: format.lib.mkMap { name = val.name; ref = val.ref; })
+                  cfg.frontends;
+                defaultText = literalExpression ''
+                  lib.mapAttrs (key: val:
+                    (pkgs.formats.elixirConf { }).lib.mkMap { name = val.name; ref = val.ref; })
+                    config.services.akkoma.frontends;
+                '';
+                description = mdDoc ''
+                  Frontend configuration.
+
+                  Users should rely on the default value and prefer to configure frontends through
+                  [{option}`config.services.akkoma.frontends`](#opt-services.akkoma.frontends).
+                '';
+              };
+            };
+
+            ":web_push_encryption" = mkOption {
+              default = { };
+              description = mdDoc ''
+                Web Push Notifications configuration.
+
+                The necessary key pair can be generated as follows:
+
+                ```ShellSession
+                $ nix-shell -p nodejs --run 'npx web-push generate-vapid-keys'
+                ```
+              '';
+              type = types.submodule {
+                freeformType = elixirValue;
+                options = {
+                  ":vapid_details" = {
+                    subject = mkOption {
+                      type = types.nonEmptyStr;
+                      default = "mailto:${ex.":pleroma".":instance".email}";
+                      defaultText = literalExpression ''
+                        "mailto:''${config.services.akkoma.config.":pleroma".":instance".email}"
+                      '';
+                      description = mdDoc "mailto URI for administrative contact.";
+                    };
+
+                    public_key = mkOption {
+                      type = with types; either nonEmptyStr secret;
+                      default = { _secret = "/var/lib/secrets/akkoma/vapid-public"; };
+                      description = mdDoc "base64-encoded public ECDH key.";
+                    };
+
+                    private_key = mkOption {
+                      type = secret;
+                      default = { _secret = "/var/lib/secrets/akkoma/vapid-private"; };
+                      description = mdDoc ''
+                        base64-encoded private ECDH key.
+
+                        The attribute `_secret` should point to a file containing the secret.
+                      '';
+                    };
+                  };
+                };
+              };
+            };
+
+            ":joken" = {
+              ":default_signer" = mkOption {
+                type = secret;
+                default = { _secret = "/var/lib/secrets/akkoma/jwt-signer"; };
+                description = mdDoc ''
+                  JWT signing secret.
+
+                  The attribute `_secret` should point to a file containing the secret.
+
+                  This secret can be generated as follows:
+
+                  ```ShellSession
+                  $ tr -dc 'A-Za-z0-9-._~' </dev/urandom | head -c 64
+                  ```
+                '';
+              };
+            };
+
+            ":logger" = {
+              ":backends" = mkOption {
+                type = types.listOf elixirValue;
+                visible = false;
+                default = with format.lib; [
+                  (mkTuple [ (mkRaw "ExSyslogger") (mkAtom ":ex_syslogger") ])
+                ];
+              };
+
+              ":ex_syslogger" = {
+                ident = mkOption {
+                  type = types.str;
+                  visible = false;
+                  default = "akkoma";
+                };
+
+                level = mkOption {
+                  type = types.nonEmptyStr;
+                  apply = format.lib.mkAtom;
+                  default = ":info";
+                  example = ":warning";
+                  description = mdDoc ''
+                    Log level.
+
+                    Refer to
+                    <https://hexdocs.pm/logger/Logger.html#module-levels>
+                    for options.
+                  '';
+                };
+              };
+            };
+
+            ":tzdata" = {
+              ":data_dir" = mkOption {
+                type = elixirValue;
+                internal = true;
+                default = format.lib.mkRaw ''
+                  Path.join(System.fetch_env!("CACHE_DIRECTORY"), "tzdata")
+                '';
+              };
+            };
+          };
+        };
+      };
+
+      nginx = mkOption {
+        type = with types; nullOr (submodule
+          (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }));
+        default = null;
+        description = mdDoc ''
+          Extra configuration for the nginx virtual host of Akkoma.
+
+          If set to `null`, no virtual host will be added to the nginx configuration.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    warnings = optionals (!config.security.sudo.enable) [''
+      The pleroma_ctl wrapper enabled by the installWrapper option relies on
+      sudo, which appears to have been disabled through security.sudo.enable.
+    ''];
+
+    users = {
+      users."${cfg.user}" = {
+        description = "Akkoma user";
+        group = cfg.group;
+        isSystemUser = true;
+      };
+      groups."${cfg.group}" = { };
+    };
+
+    # Confinement of the main service unit requires separation of the
+    # configuration generation into a separate unit to permit access to secrets
+    # residing outside of the chroot.
+    systemd.services.akkoma-config = {
+      description = "Akkoma social network configuration";
+      reloadTriggers = [ configFile ] ++ secretPaths;
+
+      unitConfig.PropagatesReloadTo = [ "akkoma.service" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        UMask = "0077";
+
+        RuntimeDirectory = "akkoma";
+
+        ExecStart = mkMerge [
+          (mkIf (cfg.dist.cookie == null) [ genScript ])
+          (mkIf (cfg.dist.cookie != null) [ copyScript ])
+          (mkIf cfg.initSecrets [ initSecretsScript ])
+          [ configScript ]
+        ];
+
+        ExecReload = mkMerge [
+          (mkIf cfg.initSecrets [ initSecretsScript ])
+          [ configScript ]
+        ];
+      };
+    };
+
+    systemd.services.akkoma-initdb = mkIf cfg.initDb.enable {
+      description = "Akkoma social network database setup";
+      requires = [ "akkoma-config.service" ];
+      requiredBy = [ "akkoma.service" ];
+      after = [ "akkoma-config.service" "postgresql.service" ];
+      before = [ "akkoma.service" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = mkIf (db ? socket_dir || db ? socket)
+          cfg.initDb.username;
+        RemainAfterExit = true;
+        UMask = "0077";
+        ExecStart = initDbScript;
+        PrivateTmp = true;
+      };
+    };
+
+    systemd.services.akkoma = let
+      runtimeInputs = with pkgs; [ coreutils gawk gnused ] ++ cfg.extraPackages;
+    in {
+      description = "Akkoma social network";
+      documentation = [ "https://docs.akkoma.dev/stable/" ];
+
+      # This service depends on network-online.target and is sequenced after
+      # it because it requires access to the Internet to function properly.
+      bindsTo = [ "akkoma-config.service" ];
+      wants = [ "network-online.service" ];
+      wantedBy = [ "multi-user.target" ];
+      after = [
+        "akkoma-config.target"
+        "network.target"
+        "network-online.target"
+        "postgresql.service"
+      ];
+
+      confinement.packages = mkIf isConfined runtimeInputs;
+      path = runtimeInputs;
+
+      serviceConfig = {
+        Type = "exec";
+        User = cfg.user;
+        Group = cfg.group;
+        UMask = "0077";
+
+        # The run‐time directory is preserved as it is managed by the akkoma-config.service unit.
+        RuntimeDirectory = "akkoma";
+        RuntimeDirectoryPreserve = true;
+
+        CacheDirectory = "akkoma";
+
+        BindPaths = [ "${uploadDir}:${uploadDir}:norbind" ];
+        BindReadOnlyPaths = mkMerge [
+          (mkIf (!isStorePath staticDir) [ "${staticDir}:${staticDir}:norbind" ])
+          (mkIf isConfined (mkMerge [
+            [ "/etc/hosts" "/etc/resolv.conf" ]
+            (mkIf (isStorePath staticDir) (map (dir: "${dir}:${dir}:norbind")
+              (splitString "\n" (readFile ((pkgs.closureInfo { rootPaths = staticDir; }) + "/store-paths")))))
+            (mkIf (db ? socket_dir) [ "${db.socket_dir}:${db.socket_dir}:norbind" ])
+            (mkIf (db ? socket) [ "${db.socket}:${db.socket}:norbind" ])
+          ]))
+        ];
+
+        ExecStartPre = "${envWrapper}/bin/pleroma_ctl migrate";
+        ExecStart = "${envWrapper}/bin/pleroma start";
+        ExecStartPost = socketScript;
+        ExecStop = "${envWrapper}/bin/pleroma stop";
+        ExecStopPost = mkIf (isAbsolutePath web.http.ip)
+          "${pkgs.coreutils}/bin/rm -f '${web.http.ip}'";
+
+        ProtectProc = "noaccess";
+        ProcSubset = "pid";
+        ProtectSystem = mkIf (!isConfined) "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        PrivateIPC = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+
+        CapabilityBoundingSet = mkIf
+          (any (port: port > 0 && port < 1024)
+            [ web.http.port cfg.dist.epmdPort cfg.dist.portMin ])
+          [ "CAP_NET_BIND_SERVICE" ];
+
+        NoNewPrivileges = true;
+        SystemCallFilter = [ "@system-service" "~@privileged" "@chown" ];
+        SystemCallArchitectures = "native";
+
+        DeviceAllow = null;
+        DevicePolicy = "closed";
+
+        # SMTP adapter uses dynamic port 0 binding, which is incompatible with bind address filtering
+        SocketBindAllow = mkIf (!hasSmtp) (mkMerge [
+          [ "tcp:${toString cfg.dist.epmdPort}" "tcp:${toString cfg.dist.portMin}-${toString cfg.dist.portMax}" ]
+          (mkIf (web.http.port != 0) [ "tcp:${toString web.http.port}" ])
+        ]);
+        SocketBindDeny = mkIf (!hasSmtp) "any";
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${uploadDir}  0700 ${cfg.user} ${cfg.group} - -"
+      "Z ${uploadDir} ~0700 ${cfg.user} ${cfg.group} - -"
+    ];
+
+    environment.systemPackages = mkIf (cfg.installWrapper) [ userWrapper ];
+
+    services.nginx.virtualHosts = mkIf (cfg.nginx != null) {
+      ${web.url.host} = mkMerge [ cfg.nginx {
+        locations."/" = {
+          proxyPass =
+            if isAbsolutePath web.http.ip
+              then "http://unix:${web.http.ip}"
+              else if hasInfix ":" web.http.ip
+                then "http://[${web.http.ip}]:${toString web.http.port}"
+                else "http://${web.http.ip}:${toString web.http.port}";
+
+          proxyWebsockets = true;
+          recommendedProxySettings = true;
+        };
+      }];
+    };
+  };
+
+  meta.maintainers = with maintainers; [ mvs ];
+  meta.doc = ./akkoma.md;
+}
diff --git a/nixos/modules/services/web-apps/alps.nix b/nixos/modules/services/web-apps/alps.nix
index 1a58df2da1d..05fb676102d 100644
--- a/nixos/modules/services/web-apps/alps.nix
+++ b/nixos/modules/services/web-apps/alps.nix
@@ -84,7 +84,7 @@ in {
         "-addr" "${cfg.bindIP}:${toString cfg.port}"
         "-theme" "${cfg.theme}"
         "imaps://${cfg.imaps.host}:${toString cfg.imaps.port}"
-        "smpts://${cfg.smtps.host}:${toString cfg.smtps.port}"
+        "smtps://${cfg.smtps.host}:${toString cfg.smtps.port}"
       ];
     };
   };
diff --git a/nixos/modules/services/web-apps/baget.nix b/nixos/modules/services/web-apps/baget.nix
deleted file mode 100644
index e4d5a1faddb..00000000000
--- a/nixos/modules/services/web-apps/baget.nix
+++ /dev/null
@@ -1,170 +0,0 @@
-{ config, lib, pkgs, ... }:
-
-with lib;
-
-let
-
-  cfg = config.services.baget;
-
-  defaultConfig = {
-    "PackageDeletionBehavior" = "Unlist";
-    "AllowPackageOverwrites" = false;
-
-    "Database" = {
-      "Type" = "Sqlite";
-      "ConnectionString" = "Data Source=baget.db";
-    };
-
-    "Storage" = {
-      "Type" = "FileSystem";
-      "Path" = "";
-    };
-
-    "Search" = {
-      "Type" = "Database";
-    };
-
-    "Mirror" = {
-      "Enabled" = false;
-      "PackageSource" = "https://api.nuget.org/v3/index.json";
-    };
-
-    "Logging" = {
-      "IncludeScopes" = false;
-      "Debug" = {
-        "LogLevel" = {
-          "Default" = "Warning";
-        };
-      };
-      "Console" = {
-        "LogLevel" = {
-          "Microsoft.Hosting.Lifetime" = "Information";
-          "Default" = "Warning";
-        };
-      };
-    };
-  };
-
-  configAttrs = recursiveUpdate defaultConfig cfg.extraConfig;
-
-  configFormat = pkgs.formats.json {};
-  configFile = configFormat.generate "appsettings.json" configAttrs;
-
-in
-{
-  options.services.baget = {
-    enable = mkEnableOption (lib.mdDoc "BaGet NuGet-compatible server");
-
-    apiKeyFile = mkOption {
-      type = types.path;
-      example = "/root/baget.key";
-      description = lib.mdDoc ''
-        Private API key for BaGet.
-      '';
-    };
-
-    extraConfig = mkOption {
-      type = configFormat.type;
-      default = {};
-      example = {
-        "Database" = {
-          "Type" = "PostgreSql";
-          "ConnectionString" = "Server=/run/postgresql;Port=5432;";
-        };
-      };
-      defaultText = literalExpression ''
-        {
-          "PackageDeletionBehavior" = "Unlist";
-          "AllowPackageOverwrites" = false;
-
-          "Database" = {
-            "Type" = "Sqlite";
-            "ConnectionString" = "Data Source=baget.db";
-          };
-
-          "Storage" = {
-            "Type" = "FileSystem";
-            "Path" = "";
-          };
-
-          "Search" = {
-            "Type" = "Database";
-          };
-
-          "Mirror" = {
-            "Enabled" = false;
-            "PackageSource" = "https://api.nuget.org/v3/index.json";
-          };
-
-          "Logging" = {
-            "IncludeScopes" = false;
-            "Debug" = {
-              "LogLevel" = {
-                "Default" = "Warning";
-              };
-            };
-            "Console" = {
-              "LogLevel" = {
-                "Microsoft.Hosting.Lifetime" = "Information";
-                "Default" = "Warning";
-              };
-            };
-          };
-        }
-      '';
-      description = lib.mdDoc ''
-        Extra configuration options for BaGet. Refer to <https://loic-sharma.github.io/BaGet/configuration/> for details.
-        Default value is merged with values from here.
-      '';
-    };
-  };
-
-  # implementation
-
-  config = mkIf cfg.enable {
-
-    systemd.services.baget = {
-      description = "BaGet server";
-      wantedBy = [ "multi-user.target" ];
-      wants = [ "network-online.target" ];
-      after = [ "network.target" "network-online.target" ];
-      path = [ pkgs.jq ];
-      serviceConfig = {
-        WorkingDirectory = "/var/lib/baget";
-        DynamicUser = true;
-        StateDirectory = "baget";
-        StateDirectoryMode = "0700";
-        LoadCredential = "api_key:${cfg.apiKeyFile}";
-
-        CapabilityBoundingSet = "";
-        NoNewPrivileges = true;
-        PrivateDevices = true;
-        PrivateTmp = true;
-        PrivateUsers = true;
-        PrivateMounts = true;
-        ProtectHome = true;
-        ProtectClock = true;
-        ProtectProc = "noaccess";
-        ProcSubset = "pid";
-        ProtectKernelLogs = true;
-        ProtectKernelModules = true;
-        ProtectKernelTunables = true;
-        ProtectControlGroups = true;
-        ProtectHostname = true;
-        RestrictSUIDSGID = true;
-        RestrictRealtime = true;
-        RestrictNamespaces = true;
-        LockPersonality = true;
-        RemoveIPC = true;
-        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
-        SystemCallFilter = [ "@system-service" "~@privileged" ];
-      };
-      script = ''
-        jq --slurpfile apiKeys <(jq -R . "$CREDENTIALS_DIRECTORY/api_key") '.ApiKey = $apiKeys[0]' ${configFile} > appsettings.json
-        ln -snf ${pkgs.baget}/lib/BaGet/wwwroot wwwroot
-        exec ${pkgs.baget}/bin/BaGet
-      '';
-    };
-
-  };
-}
diff --git a/nixos/modules/services/web-apps/changedetection-io.nix b/nixos/modules/services/web-apps/changedetection-io.nix
index fc00aee4351..bbf4c2aed18 100644
--- a/nixos/modules/services/web-apps/changedetection-io.nix
+++ b/nixos/modules/services/web-apps/changedetection-io.nix
@@ -214,7 +214,7 @@ in
           };
         })
       ];
-      podman.defaultNetwork.dnsname.enable = true;
+      podman.defaultNetwork.settings.dns_enabled = true;
     };
   };
 }
diff --git a/nixos/modules/services/web-apps/chatgpt-retrieval-plugin.nix b/nixos/modules/services/web-apps/chatgpt-retrieval-plugin.nix
new file mode 100644
index 00000000000..f29d095bc10
--- /dev/null
+++ b/nixos/modules/services/web-apps/chatgpt-retrieval-plugin.nix
@@ -0,0 +1,106 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.chatgpt-retrieval-plugin;
+in
+{
+  options.services.chatgpt-retrieval-plugin = {
+    enable = mkEnableOption (lib.mdDoc "chatgpt-retrieval-plugin service");
+
+    port = mkOption {
+      type = types.port;
+      default = 8080;
+      description = lib.mdDoc "Port the chatgpt-retrieval-plugin service listens on.";
+    };
+
+    host = mkOption {
+      type = types.str;
+      default = "127.0.0.1";
+      example = "0.0.0.0";
+      description = lib.mdDoc "The hostname or IP address for chatgpt-retrieval-plugin to bind to.";
+    };
+
+    bearerTokenPath = mkOption {
+      type = types.path;
+      description = lib.mdDoc ''
+        Path to the secret bearer token used for the http api authentication.
+      '';
+      default = "";
+      example = "config.age.secrets.CHATGPT_RETRIEVAL_PLUGIN_BEARER_TOKEN.path";
+    };
+
+    openaiApiKeyPath = mkOption {
+      type = types.path;
+      description = lib.mdDoc ''
+        Path to the secret openai api key used for embeddings.
+      '';
+      default = "";
+      example = "config.age.secrets.CHATGPT_RETRIEVAL_PLUGIN_OPENAI_API_KEY.path";
+    };
+
+    datastore = mkOption {
+      type = types.enum [ "pinecone" "weaviate" "zilliz" "milvus" "qdrant" "redis" ];
+      default = "qdrant";
+      description = lib.mdDoc "This specifies the vector database provider you want to use to store and query embeddings.";
+    };
+
+    qdrantCollection = mkOption {
+      type = types.str;
+      description = lib.mdDoc ''
+        name of the qdrant collection used to store documents.
+      '';
+      default = "document_chunks";
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = cfg.bearerTokenPath != "";
+        message = "services.chatgpt-retrieval-plugin.bearerTokenPath should not be an empty string.";
+      }
+      {
+        assertion = cfg.openaiApiKeyPath != "";
+        message = "services.chatgpt-retrieval-plugin.openaiApiKeyPath should not be an empty string.";
+      }
+    ];
+
+    systemd.services.chatgpt-retrieval-plugin = {
+      description = "ChatGPT Retrieval Plugin";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        LoadCredential = [
+          "BEARER_TOKEN:${cfg.bearerTokenPath}"
+          "OPENAI_API_KEY:${cfg.openaiApiKeyPath}"
+        ];
+        StateDirectory = "chatgpt-retrieval-plugin";
+        StateDirectoryMode = "0755";
+      };
+
+      # it doesn't make sense to pass secrets as env vars, this is a hack until
+      # upstream has proper secret management.
+      script = ''
+        export BEARER_TOKEN=$(${pkgs.systemd}/bin/systemd-creds cat BEARER_TOKEN)
+        export OPENAI_API_KEY=$(${pkgs.systemd}/bin/systemd-creds cat OPENAI_API_KEY)
+        exec ${pkgs.chatgpt-retrieval-plugin}/bin/start --host ${cfg.host} --port ${toString cfg.port}
+      '';
+
+      environment = {
+        DATASTORE = cfg.datastore;
+        QDRANT_COLLECTION = mkIf (cfg.datastore == "qdrant") cfg.qdrantCollection;
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      # create the directory for static files for fastapi
+      "C /var/lib/chatgpt-retrieval-plugin/.well-known - - - - ${pkgs.chatgpt-retrieval-plugin}/${pkgs.python3Packages.python.sitePackages}/.well-known"
+    ];
+  };
+}
diff --git a/nixos/modules/services/web-apps/cloudlog.nix b/nixos/modules/services/web-apps/cloudlog.nix
new file mode 100644
index 00000000000..da2cf93d7f1
--- /dev/null
+++ b/nixos/modules/services/web-apps/cloudlog.nix
@@ -0,0 +1,503 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.cloudlog;
+  dbFile = let
+    password = if cfg.database.createLocally
+               then "''"
+               else "trim(file_get_contents('${cfg.database.passwordFile}'))";
+  in pkgs.writeText "database.php" ''
+    <?php
+    defined('BASEPATH') OR exit('No direct script access allowed');
+    $active_group = 'default';
+    $query_builder = TRUE;
+    $db['default'] = array(
+      'dsn' => "",
+      'hostname' => '${cfg.database.host}',
+      'username' => '${cfg.database.user}',
+      'password' => ${password},
+      'database' => '${cfg.database.name}',
+      'dbdriver' => 'mysqli',
+      'dbprefix' => "",
+      'pconnect' => TRUE,
+      'db_debug' => (ENVIRONMENT !== 'production'),
+      'cache_on' => FALSE,
+      'cachedir' => "",
+      'char_set' => 'utf8mb4',
+      'dbcollat' => 'utf8mb4_general_ci',
+      'swap_pre' => "",
+      'encrypt' => FALSE,
+      'compress' => FALSE,
+      'stricton' => FALSE,
+      'failover' => array(),
+      'save_queries' => TRUE
+    );
+  '';
+  configFile = pkgs.writeText "config.php" ''
+    <?php
+    include('${pkgs.cloudlog}/install/config/config.php');
+    $config['datadir'] = "${cfg.dataDir}/";
+    $config['base_url'] = "${cfg.baseUrl}";
+    ${cfg.extraConfig}
+  '';
+  package = pkgs.stdenv.mkDerivation rec {
+    pname = "cloudlog";
+    version = src.version;
+    src = pkgs.cloudlog;
+    installPhase = ''
+      mkdir -p $out
+      cp -r * $out/
+
+      ln -s ${configFile} $out/application/config/config.php
+      ln -s ${dbFile} $out/application/config/database.php
+
+      # link writable directories
+      for directory in updates uploads backup logbook; do
+        rm -rf $out/$directory
+        ln -s ${cfg.dataDir}/$directory $out/$directory
+      done
+
+      # link writable asset files
+      for asset in dok sota wwff; do
+        rm -rf $out/assets/json/$asset.txt
+        ln -s ${cfg.dataDir}/assets/json/$asset.txt $out/assets/json/$asset.txt
+      done
+    '';
+  };
+in
+{
+  options.services.cloudlog = with types; {
+    enable = mkEnableOption (mdDoc "Whether to enable Cloudlog");
+    dataDir = mkOption {
+      type = str;
+      default = "/var/lib/cloudlog";
+      description = mdDoc "Cloudlog data directory.";
+    };
+    baseUrl = mkOption {
+      type = str;
+      default = "http://localhost";
+      description = mdDoc "Cloudlog base URL";
+    };
+    user = mkOption {
+      type = str;
+      default = "cloudlog";
+      description = mdDoc "User account under which Cloudlog runs.";
+    };
+    database = {
+      createLocally = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Create the database and database user locally.";
+      };
+      host = mkOption {
+        type = str;
+        description = mdDoc "MySQL database host";
+        default = "localhost";
+      };
+      name = mkOption {
+        type = str;
+        description = mdDoc "MySQL database name.";
+        default = "cloudlog";
+      };
+      user = mkOption {
+        type = str;
+        description = mdDoc "MySQL user name.";
+        default = "cloudlog";
+      };
+      passwordFile = mkOption {
+        type = nullOr str;
+        description = mdDoc "MySQL user password file.";
+        default = null;
+      };
+    };
+    poolConfig = mkOption {
+      type = 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 = mdDoc ''
+        Options for Cloudlog's PHP-FPM pool.
+      '';
+    };
+    virtualHost = mkOption {
+      type = nullOr str;
+      default = "localhost";
+      description = mdDoc ''
+        Name of the nginx virtualhost to use and setup. If null, do not setup
+         any virtualhost.
+      '';
+    };
+    extraConfig = mkOption {
+      description = mdDoc ''
+       Any additional text to be appended to the config.php
+       configuration file. This is a PHP script. For configuration
+       settings, see <https://github.com/magicbug/Cloudlog/wiki/Cloudlog.php-Configuration-File>.
+      '';
+      default = "";
+      type = str;
+      example = ''
+        $config['show_time'] = TRUE;
+      '';
+    };
+    upload-lotw = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically upload logs to LoTW. If enabled, a systemd
+          timer will run the log upload task as specified by the interval
+           option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "daily";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the
+          time at which the LoTW upload will occur.
+        '';
+      };
+    };
+    upload-clublog = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically upload logs to Clublog. If enabled, a systemd
+          timer will run the log upload task as specified by the interval option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "daily";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the time
+          at which the Clublog upload will occur.
+        '';
+      };
+    };
+    update-lotw-users = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically update the list of LoTW users. If enabled, a
+          systemd timer will run the update task as specified by the interval
+          option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "weekly";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the
+          time at which the LoTW user update will occur.
+        '';
+      };
+    };
+    update-dok = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically update the DOK resource file. If enabled, a
+          systemd timer will run the update task as specified by the interval option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "monthly";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the
+          time at which the DOK update will occur.
+        '';
+      };
+    };
+    update-clublog-scp = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically update the Clublog SCP database. If enabled,
+          a systemd timer will run the update task as specified by the interval
+          option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "monthly";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the time
+          at which the Clublog SCP update will occur.
+        '';
+      };
+    };
+    update-wwff = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically update the WWFF database. If enabled, a
+          systemd timer will run the update task as specified by the interval
+          option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "monthly";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the time
+          at which the WWFF update will occur.
+        '';
+      };
+    };
+    upload-qrz = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically upload logs to QRZ. If enabled, a systemd
+          timer will run the update task as specified by the interval option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "daily";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the
+          time at which the QRZ upload will occur.
+        '';
+      };
+    };
+    update-sota = {
+      enable = mkOption {
+        type = bool;
+        default = true;
+        description = mdDoc ''
+          Whether to periodically update the SOTA database. If enabled, a
+          systemd timer will run the update task as specified by the interval option.
+        '';
+      };
+      interval = mkOption {
+        type = str;
+        default = "monthly";
+        description = mdDoc ''
+          Specification (in the format described by systemd.time(7)) of the time
+          at which the SOTA update will occur.
+        '';
+      };
+    };
+  };
+  config = mkIf cfg.enable {
+
+    assertions = [
+      {
+        assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
+        message = "services.cloudlog.database.passwordFile cannot be specified if services.cloudlog.database.createLocally is set to true.";
+      }
+    ];
+
+    services.phpfpm = {
+      pools.cloudlog = {
+        inherit (cfg) user;
+        group = config.services.nginx.group;
+        settings =  {
+          "listen.owner" = config.services.nginx.user;
+          "listen.group" = config.services.nginx.group;
+        } // cfg.poolConfig;
+      };
+    };
+
+    services.nginx = mkIf (cfg.virtualHost != null) {
+      enable = true;
+      virtualHosts = {
+        "${cfg.virtualHost}" = {
+          root = "${package}";
+          locations."/".tryFiles = "$uri /index.php$is_args$args";
+          locations."~ ^/index.php(/|$)".extraConfig = ''
+              include ${config.services.nginx.package}/conf/fastcgi_params;
+              include ${pkgs.nginx}/conf/fastcgi.conf;
+              fastcgi_split_path_info ^(.+\.php)(.+)$;
+              fastcgi_pass unix:${config.services.phpfpm.pools.cloudlog.socket};
+              fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+            '';
+        };
+      };
+    };
+
+    services.mysql = mkIf cfg.database.createLocally {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [{
+        name = cfg.database.user;
+        ensurePermissions = {
+          "${cfg.database.name}.*" = "ALL PRIVILEGES";
+        };
+      }];
+    };
+
+    systemd = {
+      services = {
+        cloudlog-setup-database = mkIf cfg.database.createLocally {
+          description = "Set up cloudlog database";
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = true;
+          };
+          wantedBy = [ "phpfpm-cloudlog.service" ];
+          after = [ "mysql.service" ];
+          script = let
+            mysql = "${config.services.mysql.package}/bin/mysql";
+          in ''
+            if [ ! -f ${cfg.dataDir}/.dbexists ]; then
+              ${mysql} ${cfg.database.name} < ${pkgs.cloudlog}/install/assets/install.sql
+              touch ${cfg.dataDir}/.dbexists
+            fi
+        '';
+        };
+        cloudlog-upload-lotw = {
+          description = "Upload QSOs to LoTW if certs have been provided";
+          enable = cfg.upload-lotw.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/lotw/lotw_upload";
+        };
+        cloudlog-update-lotw-users = {
+          description = "Update LOTW Users Database";
+          enable = cfg.update-lotw-users.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/lotw/load_users";
+        };
+        cloudlog-update-dok = {
+          description = "Update DOK File for autocomplete";
+          enable = cfg.update-dok.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/update/update_dok";
+        };
+        cloudlog-update-clublog-scp = {
+          description = "Update Clublog SCP Database File";
+          enable = cfg.update-clublog-scp.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/update/update_clublog_scp";
+        };
+        cloudlog-update-wwff = {
+          description = "Update WWFF File for autocomplete";
+          enable = cfg.update-wwff.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/update/update_wwff";
+        };
+        cloudlog-upload-qrz = {
+          description = "Upload QSOs to QRZ Logbook";
+          enable = cfg.upload-qrz.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/qrz/upload";
+        };
+        cloudlog-update-sota = {
+          description = "Update SOTA File for autocomplete";
+          enable = cfg.update-sota.enable;
+          script = "${pkgs.curl}/bin/curl -s ${cfg.baseUrl}/update/update_sota";
+        };
+      };
+      timers = {
+        cloudlog-upload-lotw = {
+          enable = cfg.upload-lotw.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-upload-lotw.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.upload-lotw.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-upload-clublog = {
+          enable = cfg.upload-clublog.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-upload-clublog.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.upload-clublog.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-update-lotw-users = {
+          enable = cfg.update-lotw-users.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-update-lotw-users.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.update-lotw-users.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-update-dok = {
+          enable = cfg.update-dok.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-update-dok.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.update-dok.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-update-clublog-scp = {
+          enable = cfg.update-clublog-scp.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-update-clublog-scp.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.update-clublog-scp.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-update-wwff =  {
+          enable = cfg.update-wwff.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-update-wwff.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.update-wwff.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-upload-qrz = {
+          enable = cfg.upload-qrz.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-upload-qrz.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.upload-qrz.interval;
+            Persistent = true;
+          };
+        };
+        cloudlog-update-sota = {
+          enable = cfg.update-sota.enable;
+          wantedBy = [ "timers.target" ];
+          partOf = [ "cloudlog-update-sota.service" ];
+          after = [ "phpfpm-cloudlog.service" ];
+          timerConfig = {
+            OnCalendar = cfg.update-sota.interval;
+            Persistent = true;
+          };
+        };
+      };
+      tmpfiles.rules = let
+        group = config.services.nginx.group;
+      in [
+        "d ${cfg.dataDir}                0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/updates        0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/uploads        0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/backup         0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/logbook        0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/assets/json    0750 ${cfg.user} ${group} - -"
+        "d ${cfg.dataDir}/assets/qslcard 0750 ${cfg.user} ${group} - -"
+      ];
+    };
+
+    users.users."${cfg.user}" = {
+      isSystemUser = true;
+      group = config.services.nginx.group;
+    };
+  };
+
+  meta.maintainers = with maintainers; [ melling ];
+}
diff --git a/nixos/modules/services/web-apps/coder.nix b/nixos/modules/services/web-apps/coder.nix
new file mode 100644
index 00000000000..469a29bc3aa
--- /dev/null
+++ b/nixos/modules/services/web-apps/coder.nix
@@ -0,0 +1,217 @@
+{ config, lib, options, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.coder;
+  name = "coder";
+in {
+  options = {
+    services.coder = {
+      enable = mkEnableOption (lib.mdDoc "Coder service");
+
+      user = mkOption {
+        type = types.str;
+        default = "coder";
+        description = lib.mdDoc ''
+          User under which the coder service runs.
+
+          ::: {.note}
+          If left as the default value this user will automatically be created
+          on system activation, otherwise it needs to be configured manually.
+          :::
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "coder";
+        description = lib.mdDoc ''
+          Group under which the coder service runs.
+
+          ::: {.note}
+          If left as the default value this group will automatically be created
+          on system activation, otherwise it needs to be configured manually.
+          :::
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.coder;
+        description = lib.mdDoc ''
+          Package to use for the service.
+        '';
+        defaultText = literalExpression "pkgs.coder";
+      };
+
+      homeDir = mkOption {
+        type = types.str;
+        description = lib.mdDoc ''
+          Home directory for coder user.
+        '';
+        default = "/var/lib/coder";
+      };
+
+      listenAddress = mkOption {
+        type = types.str;
+        description = lib.mdDoc ''
+          Listen address.
+        '';
+        default = "127.0.0.1:3000";
+      };
+
+      accessUrl = mkOption {
+        type = types.nullOr types.str;
+        description = lib.mdDoc ''
+          Access URL should be a external IP address or domain with DNS records pointing to Coder.
+        '';
+        default = null;
+        example = "https://coder.example.com";
+      };
+
+      wildcardAccessUrl = mkOption {
+        type = types.nullOr types.str;
+        description = lib.mdDoc ''
+          If you are providing TLS certificates directly to the Coder server, you must use a single certificate for the root and wildcard domains.
+        '';
+        default = null;
+        example = "*.coder.example.com";
+      };
+
+      database = {
+        createLocally = mkOption {
+          type = types.bool;
+          default = true;
+          description = lib.mdDoc ''
+            Create the database and database user locally.
+          '';
+        };
+
+        host = mkOption {
+          type = types.str;
+          default = "/run/postgresql";
+          description = lib.mdDoc ''
+            Hostname hosting the database.
+          '';
+        };
+
+        database = mkOption {
+          type = types.str;
+          default = "coder";
+          description = lib.mdDoc ''
+            Name of database.
+          '';
+        };
+
+        username = mkOption {
+          type = types.str;
+          default = "coder";
+          description = lib.mdDoc ''
+            Username for accessing the database.
+          '';
+        };
+
+        password = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = lib.mdDoc ''
+            Password for accessing the database.
+          '';
+        };
+
+        sslmode = mkOption {
+          type = types.nullOr types.str;
+          default = "disable";
+          description = lib.mdDoc ''
+            Password for accessing the database.
+          '';
+        };
+      };
+
+      tlsCert = mkOption {
+        type = types.nullOr types.path;
+        description = lib.mdDoc ''
+          The path to the TLS certificate.
+        '';
+        default = null;
+      };
+
+      tlsKey = mkOption {
+        type = types.nullOr types.path;
+        description = lib.mdDoc ''
+          The path to the TLS key.
+        '';
+        default = null;
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = cfg.database.createLocally -> cfg.database.username == name;
+        message = "services.coder.database.username must be set to ${user} if services.coder.database.createLocally is set true";
+      }
+    ];
+
+    systemd.services.coder = {
+      description = "Coder - Self-hosted developer workspaces on your infra";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        CODER_ACCESS_URL = cfg.accessUrl;
+        CODER_WILDCARD_ACCESS_URL = cfg.wildcardAccessUrl;
+        CODER_PG_CONNECTION_URL = "user=${cfg.database.username} ${optionalString (cfg.database.password != null) "password=${cfg.database.password}"} database=${cfg.database.database} host=${cfg.database.host} ${optionalString (cfg.database.sslmode != null) "sslmode=${cfg.database.sslmode}"}";
+        CODER_ADDRESS = cfg.listenAddress;
+        CODER_TLS_ENABLE = optionalString (cfg.tlsCert != null) "1";
+        CODER_TLS_CERT_FILE = cfg.tlsCert;
+        CODER_TLS_KEY_FILE = cfg.tlsKey;
+      };
+
+      serviceConfig = {
+        ProtectSystem = "full";
+        PrivateTmp = "yes";
+        PrivateDevices = "yes";
+        SecureBits = "keep-caps";
+        AmbientCapabilities = "CAP_IPC_LOCK CAP_NET_BIND_SERVICE";
+        CacheDirectory = "coder";
+        CapabilityBoundingSet = "CAP_SYSLOG CAP_IPC_LOCK CAP_NET_BIND_SERVICE";
+        KillSignal = "SIGINT";
+        KillMode = "mixed";
+        NoNewPrivileges = "yes";
+        Restart = "on-failure";
+        ExecStart = "${cfg.package}/bin/coder server";
+        User = cfg.user;
+        Group = cfg.group;
+      };
+    };
+
+    services.postgresql = lib.mkIf cfg.database.createLocally {
+      enable = true;
+      ensureDatabases = [
+        cfg.database.database
+      ];
+      ensureUsers = [{
+        name = cfg.database.username;
+        ensurePermissions = {
+          "DATABASE \"${cfg.database.database}\"" = "ALL PRIVILEGES";
+        };
+        }
+      ];
+    };
+
+    users.groups = optionalAttrs (cfg.group == name) {
+      "${cfg.group}" = {};
+    };
+    users.users = optionalAttrs (cfg.user == name) {
+      ${name} = {
+        description = "Coder service user";
+        group = cfg.group;
+        home = cfg.homeDir;
+        createHome = true;
+        isSystemUser = true;
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/dex.nix b/nixos/modules/services/web-apps/dex.nix
index 1dcc6f7a7c5..f69f1749aeb 100644
--- a/nixos/modules/services/web-apps/dex.nix
+++ b/nixos/modules/services/web-apps/dex.nix
@@ -83,11 +83,12 @@ in
         AmbientCapabilities = "CAP_NET_BIND_SERVICE";
         BindReadOnlyPaths = [
           "/nix/store"
-          "-/etc/resolv.conf"
-          "-/etc/nsswitch.conf"
+          "-/etc/dex"
           "-/etc/hosts"
           "-/etc/localtime"
-          "-/etc/dex"
+          "-/etc/nsswitch.conf"
+          "-/etc/resolv.conf"
+          "-/etc/ssl/certs/ca-certificates.crt"
         ];
         BindPaths = optional (cfg.settings.storage.type == "postgres") "/var/run/postgresql";
         CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
diff --git a/nixos/modules/services/web-apps/discourse.md b/nixos/modules/services/web-apps/discourse.md
new file mode 100644
index 00000000000..35180bea87d
--- /dev/null
+++ b/nixos/modules/services/web-apps/discourse.md
@@ -0,0 +1,286 @@
+# Discourse {#module-services-discourse}
+
+[Discourse](https://www.discourse.org/) is a
+modern and open source discussion platform.
+
+## Basic usage {#module-services-discourse-basic-usage}
+
+A minimal configuration using Let's Encrypt for TLS certificates looks like this:
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+security.acme.email = "me@example.com";
+security.acme.acceptTerms = true;
+```
+
+Provided a proper DNS setup, you'll be able to connect to the
+instance at `discourse.example.com` and log in
+using the credentials provided in
+`services.discourse.admin`.
+
+## Using a regular TLS certificate {#module-services-discourse-tls}
+
+To set up TLS using a regular certificate and key on file, use
+the [](#opt-services.discourse.sslCertificate)
+and [](#opt-services.discourse.sslCertificateKey)
+options:
+
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
+
+## Database access {#module-services-discourse-database}
+
+Discourse uses PostgreSQL to store most of its
+data. A database will automatically be enabled and a database
+and role created unless [](#opt-services.discourse.database.host) is changed from
+its default of `null` or [](#opt-services.discourse.database.createLocally) is set
+to `false`.
+
+External database access can also be configured by setting
+[](#opt-services.discourse.database.host),
+[](#opt-services.discourse.database.username) and
+[](#opt-services.discourse.database.passwordFile) as
+appropriate. Note that you need to manually create a database
+called `discourse` (or the name you chose in
+[](#opt-services.discourse.database.name)) and
+allow the configured database user full access to it.
+
+## Email {#module-services-discourse-mail}
+
+In addition to the basic setup, you'll want to configure an SMTP
+server Discourse can use to send user
+registration and password reset emails, among others. You can
+also optionally let Discourse receive
+email, which enables people to reply to threads and conversations
+via email.
+
+A basic setup which assumes you want to use your configured
+[hostname](#opt-services.discourse.hostname) as
+email domain can be done like this:
+
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    serverAddress = "smtp.emailprovider.com";
+    port = 587;
+    username = "user@emailprovider.com";
+    passwordFile = "/path/to/smtp_password_file";
+  };
+  mail.incoming.enable = true;
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
+
+This assumes you have set up an MX record for the address you've
+set in [hostname](#opt-services.discourse.hostname) and
+requires proper SPF, DKIM and DMARC configuration to be done for
+the domain you're sending from, in order for email to be reliably delivered.
+
+If you want to use a different domain for your outgoing email
+(for example `example.com` instead of
+`discourse.example.com`) you should set
+[](#opt-services.discourse.mail.notificationEmailAddress) and
+[](#opt-services.discourse.mail.contactEmailAddress) manually.
+
+::: {.note}
+Setup of TLS for incoming email is currently only configured
+automatically when a regular TLS certificate is used, i.e. when
+[](#opt-services.discourse.sslCertificate) and
+[](#opt-services.discourse.sslCertificateKey) are
+set.
+:::
+
+## Additional settings {#module-services-discourse-settings}
+
+Additional site settings and backend settings, for which no
+explicit NixOS options are provided,
+can be set in [](#opt-services.discourse.siteSettings) and
+[](#opt-services.discourse.backendSettings) respectively.
+
+### Site settings {#module-services-discourse-site-settings}
+
+"Site settings" are the settings that can be
+changed through the Discourse
+UI. Their *default* values can be set using
+[](#opt-services.discourse.siteSettings).
+
+Settings are expressed as a Nix attribute set which matches the
+structure of the configuration in
+[config/site_settings.yml](https://github.com/discourse/discourse/blob/master/config/site_settings.yml).
+To find a setting's path, you only need to care about the first
+two levels; i.e. its category (e.g. `login`)
+and name (e.g. `invite_only`).
+
+Settings containing secret data should be set to an attribute
+set containing the attribute `_secret` - a
+string pointing to a file containing the value the option
+should be set to. See the example.
+
+### Backend settings {#module-services-discourse-backend-settings}
+
+Settings are expressed as a Nix attribute set which matches the
+structure of the configuration in
+[config/discourse.conf](https://github.com/discourse/discourse/blob/stable/config/discourse_defaults.conf).
+Empty parameters can be defined by setting them to
+`null`.
+
+### Example {#module-services-discourse-settings-example}
+
+The following example sets the title and description of the
+Discourse instance and enables
+GitHub login in the site settings,
+and changes a few request limits in the backend settings:
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    serverAddress = "smtp.emailprovider.com";
+    port = 587;
+    username = "user@emailprovider.com";
+    passwordFile = "/path/to/smtp_password_file";
+  };
+  mail.incoming.enable = true;
+  siteSettings = {
+    required = {
+      title = "My Cats";
+      site_description = "Discuss My Cats (and be nice plz)";
+    };
+    login = {
+      enable_github_logins = true;
+      github_client_id = "a2f6dfe838cb3206ce20";
+      github_client_secret._secret = /run/keys/discourse_github_client_secret;
+    };
+  };
+  backendSettings = {
+    max_reqs_per_ip_per_minute = 300;
+    max_reqs_per_ip_per_10_seconds = 60;
+    max_asset_reqs_per_ip_per_10_seconds = 250;
+    max_reqs_per_ip_mode = "warn+block";
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
+
+In the resulting site settings file, the
+`login.github_client_secret` key will be set
+to the contents of the
+{file}`/run/keys/discourse_github_client_secret`
+file.
+
+## Plugins {#module-services-discourse-plugins}
+
+You can install Discourse plugins
+using the [](#opt-services.discourse.plugins)
+option. Pre-packaged plugins are provided in
+`<your_discourse_package_here>.plugins`. If
+you want the full suite of plugins provided through
+`nixpkgs`, you can also set the [](#opt-services.discourse.package) option to
+`pkgs.discourseAllPlugins`.
+
+Plugins can be built with the
+`<your_discourse_package_here>.mkDiscoursePlugin`
+function. Normally, it should suffice to provide a
+`name` and `src` attribute. If
+the plugin has Ruby dependencies, however, they need to be
+packaged in accordance with the [Developing with Ruby](https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby)
+section of the Nixpkgs manual and the
+appropriate gem options set in `bundlerEnvArgs`
+(normally `gemdir` is sufficient). A plugin's
+Ruby dependencies are listed in its
+{file}`plugin.rb` file as function calls to
+`gem`. To construct the corresponding
+{file}`Gemfile` manually, run {command}`bundle init`, then add the `gem` lines to it
+verbatim.
+
+Much of the packaging can be done automatically by the
+{file}`nixpkgs/pkgs/servers/web-apps/discourse/update.py`
+script - just add the plugin to the `plugins`
+list in the `update_plugins` function and run
+the script:
+```bash
+./update.py update-plugins
+```
+
+Some plugins provide [site settings](#module-services-discourse-site-settings).
+Their defaults can be configured using [](#opt-services.discourse.siteSettings), just like
+regular site settings. To find the names of these settings, look
+in the `config/settings.yml` file of the plugin
+repo.
+
+For example, to add the [discourse-spoiler-alert](https://github.com/discourse/discourse-spoiler-alert)
+and [discourse-solved](https://github.com/discourse/discourse-solved)
+plugins, and disable `discourse-spoiler-alert`
+by default:
+
+```
+services.discourse = {
+  enable = true;
+  hostname = "discourse.example.com";
+  sslCertificate = "/path/to/ssl_certificate";
+  sslCertificateKey = "/path/to/ssl_certificate_key";
+  admin = {
+    email = "admin@example.com";
+    username = "admin";
+    fullName = "Administrator";
+    passwordFile = "/path/to/password_file";
+  };
+  mail.outgoing = {
+    serverAddress = "smtp.emailprovider.com";
+    port = 587;
+    username = "user@emailprovider.com";
+    passwordFile = "/path/to/smtp_password_file";
+  };
+  mail.incoming.enable = true;
+  plugins = with config.services.discourse.package.plugins; [
+    discourse-spoiler-alert
+    discourse-solved
+  ];
+  siteSettings = {
+    plugins = {
+      spoiler_enabled = false;
+    };
+  };
+  secretKeyBaseFile = "/path/to/secret_key_base_file";
+};
+```
diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix
index 1ab0e679a54..f80eb6b4c7f 100644
--- a/nixos/modules/services/web-apps/discourse.nix
+++ b/nixos/modules/services/web-apps/discourse.nix
@@ -615,6 +615,7 @@ in
       s3_endpoint = null;
       s3_http_continue_timeout = null;
       s3_install_cors_rule = null;
+      s3_asset_cdn_url = null;
 
       max_user_api_reqs_per_minute = 20;
       max_user_api_reqs_per_day = 2880;
@@ -647,6 +648,12 @@ in
       multisite_config_path = "config/multisite.yml";
       enable_long_polling = null;
       long_polling_interval = null;
+      preload_link_header = false;
+      redirect_avatar_requests = false;
+      pg_force_readonly_mode = false;
+      dns_query_timeout_secs = null;
+      regex_timeout_seconds = 2;
+      allow_impersonation = true;
     };
 
     services.redis.servers.discourse =
@@ -820,10 +827,10 @@ in
 
     services.nginx = lib.mkIf cfg.nginx.enable {
       enable = true;
-      additionalModules = [ pkgs.nginxModules.brotli ];
 
       recommendedTlsSettings = true;
       recommendedOptimisation = true;
+      recommendedBrotliSettings = true;
       recommendedGzipSettings = true;
       recommendedProxySettings = true;
 
@@ -1011,6 +1018,7 @@ in
         notification_email = cfg.mail.notificationEmailAddress;
         contact_email = cfg.mail.contactEmailAddress;
       };
+      security.force_https = tlsEnabled;
       email = {
         manual_polling_enabled = cfg.mail.incoming.enable;
         reply_by_email_enabled = cfg.mail.incoming.enable;
@@ -1020,8 +1028,8 @@ in
 
     services.postfix = lib.mkIf cfg.mail.incoming.enable {
       enable = true;
-      sslCert = if cfg.sslCertificate != null then cfg.sslCertificate else "";
-      sslKey = if cfg.sslCertificateKey != null then cfg.sslCertificateKey else "";
+      sslCert = lib.optionalString (cfg.sslCertificate != null) cfg.sslCertificate;
+      sslKey = lib.optionalString (cfg.sslCertificateKey != null) cfg.sslCertificateKey;
 
       origin = cfg.hostname;
       relayDomains = [ cfg.hostname ];
@@ -1080,6 +1088,6 @@ in
     ];
   };
 
-  meta.doc = ./discourse.xml;
+  meta.doc = ./discourse.md;
   meta.maintainers = [ lib.maintainers.talyz ];
 }
diff --git a/nixos/modules/services/web-apps/discourse.xml b/nixos/modules/services/web-apps/discourse.xml
deleted file mode 100644
index ad9b65abf51..00000000000
--- a/nixos/modules/services/web-apps/discourse.xml
+++ /dev/null
@@ -1,355 +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-discourse">
- <title>Discourse</title>
- <para>
-   <link xlink:href="https://www.discourse.org/">Discourse</link> is a
-   modern and open source discussion platform.
- </para>
-
- <section xml:id="module-services-discourse-basic-usage">
-   <title>Basic usage</title>
-   <para>
-     A minimal configuration using Let's Encrypt for TLS certificates looks like this:
-<programlisting>
-services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
-  };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
-};
-<link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
-<link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
-</programlisting>
-   </para>
-
-   <para>
-     Provided a proper DNS setup, you'll be able to connect to the
-     instance at <literal>discourse.example.com</literal> and log in
-     using the credentials provided in
-     <literal>services.discourse.admin</literal>.
-   </para>
- </section>
-
- <section xml:id="module-services-discourse-tls">
-   <title>Using a regular TLS certificate</title>
-   <para>
-     To set up TLS using a regular certificate and key on file, use
-     the <xref linkend="opt-services.discourse.sslCertificate" />
-     and <xref linkend="opt-services.discourse.sslCertificateKey" />
-     options:
-
-<programlisting>
-services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
-  admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
-  };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
-};
-</programlisting>
-
-   </para>
- </section>
-
- <section xml:id="module-services-discourse-database">
-   <title>Database access</title>
-   <para>
-     <productname>Discourse</productname> uses
-     <productname>PostgreSQL</productname> to store most of its
-     data. A database will automatically be enabled and a database
-     and role created unless <xref
-     linkend="opt-services.discourse.database.host" /> is changed from
-     its default of <literal>null</literal> or <xref
-     linkend="opt-services.discourse.database.createLocally" /> is set
-     to <literal>false</literal>.
-   </para>
-
-   <para>
-     External database access can also be configured by setting
-     <xref linkend="opt-services.discourse.database.host" />, <xref
-     linkend="opt-services.discourse.database.username" /> and <xref
-     linkend="opt-services.discourse.database.passwordFile" /> as
-     appropriate. Note that you need to manually create a database
-     called <literal>discourse</literal> (or the name you chose in
-     <xref linkend="opt-services.discourse.database.name" />) and
-     allow the configured database user full access to it.
-   </para>
- </section>
-
- <section xml:id="module-services-discourse-mail">
-   <title>Email</title>
-   <para>
-     In addition to the basic setup, you'll want to configure an SMTP
-     server <productname>Discourse</productname> can use to send user
-     registration and password reset emails, among others. You can
-     also optionally let <productname>Discourse</productname> receive
-     email, which enables people to reply to threads and conversations
-     via email.
-   </para>
-
-   <para>
-     A basic setup which assumes you want to use your configured <link
-     linkend="opt-services.discourse.hostname">hostname</link> as
-     email domain can be done like this:
-
-<programlisting>
-services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
-  admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
-  };
-  mail.outgoing = {
-    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
-    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
-  };
-  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
-};
-</programlisting>
-
-     This assumes you have set up an MX record for the address you've
-     set in <link linkend="opt-services.discourse.hostname">hostname</link> and
-     requires proper SPF, DKIM and DMARC configuration to be done for
-     the domain you're sending from, in order for email to be reliably delivered.
-   </para>
-
-   <para>
-     If you want to use a different domain for your outgoing email
-     (for example <literal>example.com</literal> instead of
-     <literal>discourse.example.com</literal>) you should set
-     <xref linkend="opt-services.discourse.mail.notificationEmailAddress" /> and
-     <xref linkend="opt-services.discourse.mail.contactEmailAddress" /> manually.
-   </para>
-
-   <note>
-     <para>
-       Setup of TLS for incoming email is currently only configured
-       automatically when a regular TLS certificate is used, i.e. when
-       <xref linkend="opt-services.discourse.sslCertificate" /> and
-       <xref linkend="opt-services.discourse.sslCertificateKey" /> are
-       set.
-     </para>
-   </note>
-
- </section>
-
- <section xml:id="module-services-discourse-settings">
-   <title>Additional settings</title>
-   <para>
-     Additional site settings and backend settings, for which no
-     explicit <productname>NixOS</productname> options are provided,
-     can be set in <xref linkend="opt-services.discourse.siteSettings" /> and
-     <xref linkend="opt-services.discourse.backendSettings" /> respectively.
-   </para>
-
-   <section xml:id="module-services-discourse-site-settings">
-     <title>Site settings</title>
-     <para>
-       <quote>Site settings</quote> are the settings that can be
-       changed through the <productname>Discourse</productname>
-       UI. Their <emphasis>default</emphasis> values can be set using
-       <xref linkend="opt-services.discourse.siteSettings" />.
-     </para>
-
-     <para>
-       Settings are expressed as a Nix attribute set which matches the
-       structure of the configuration in
-       <link xlink:href="https://github.com/discourse/discourse/blob/master/config/site_settings.yml">config/site_settings.yml</link>.
-       To find a setting's path, you only need to care about the first
-       two levels; i.e. its category (e.g. <literal>login</literal>)
-       and name (e.g. <literal>invite_only</literal>).
-     </para>
-
-     <para>
-       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.
-     </para>
-   </section>
-
-   <section xml:id="module-services-discourse-backend-settings">
-     <title>Backend settings</title>
-     <para>
-       Settings are expressed as a Nix attribute set which matches the
-       structure of the configuration in
-       <link xlink:href="https://github.com/discourse/discourse/blob/stable/config/discourse_defaults.conf">config/discourse.conf</link>.
-       Empty parameters can be defined by setting them to
-       <literal>null</literal>.
-     </para>
-   </section>
-
-   <section xml:id="module-services-discourse-settings-example">
-     <title>Example</title>
-     <para>
-       The following example sets the title and description of the
-       <productname>Discourse</productname> instance and enables
-       <productname>GitHub</productname> login in the site settings,
-       and changes a few request limits in the backend settings:
-<programlisting>
-services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
-  admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
-  };
-  mail.outgoing = {
-    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
-    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
-  };
-  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
-    required = {
-      title = "My Cats";
-      site_description = "Discuss My Cats (and be nice plz)";
-    };
-    login = {
-      enable_github_logins = true;
-      github_client_id = "a2f6dfe838cb3206ce20";
-      github_client_secret._secret = /run/keys/discourse_github_client_secret;
-    };
-  };
-  <link linkend="opt-services.discourse.backendSettings">backendSettings</link> = {
-    max_reqs_per_ip_per_minute = 300;
-    max_reqs_per_ip_per_10_seconds = 60;
-    max_asset_reqs_per_ip_per_10_seconds = 250;
-    max_reqs_per_ip_mode = "warn+block";
-  };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
-};
-</programlisting>
-     </para>
-     <para>
-       In the resulting site settings file, the
-       <literal>login.github_client_secret</literal> key will be set
-       to the contents of the
-       <filename>/run/keys/discourse_github_client_secret</filename>
-       file.
-     </para>
-   </section>
- </section>
-  <section xml:id="module-services-discourse-plugins">
-    <title>Plugins</title>
-    <para>
-      You can install <productname>Discourse</productname> plugins
-      using the <xref linkend="opt-services.discourse.plugins" />
-      option. Pre-packaged plugins are provided in
-      <literal>&lt;your_discourse_package_here&gt;.plugins</literal>. If
-      you want the full suite of plugins provided through
-      <literal>nixpkgs</literal>, you can also set the <xref
-      linkend="opt-services.discourse.package" /> option to
-      <literal>pkgs.discourseAllPlugins</literal>.
-    </para>
-
-    <para>
-      Plugins can be built with the
-      <literal>&lt;your_discourse_package_here&gt;.mkDiscoursePlugin</literal>
-      function. Normally, it should suffice to provide a
-      <literal>name</literal> and <literal>src</literal> attribute. If
-      the plugin has Ruby dependencies, however, they need to be
-      packaged in accordance with the <link
-      xlink:href="https://nixos.org/manual/nixpkgs/stable/#developing-with-ruby">Developing
-      with Ruby</link> section of the Nixpkgs manual and the
-      appropriate gem options set in <literal>bundlerEnvArgs</literal>
-      (normally <literal>gemdir</literal> is sufficient). A plugin's
-      Ruby dependencies are listed in its
-      <filename>plugin.rb</filename> file as function calls to
-      <literal>gem</literal>. To construct the corresponding
-      <filename>Gemfile</filename> manually, run <command>bundle
-      init</command>, then add the <literal>gem</literal> lines to it
-      verbatim.
-    </para>
-
-    <para>
-      Much of the packaging can be done automatically by the
-      <filename>nixpkgs/pkgs/servers/web-apps/discourse/update.py</filename>
-      script - just add the plugin to the <literal>plugins</literal>
-      list in the <function>update_plugins</function> function and run
-      the script:
-      <programlisting language="bash">
-./update.py update-plugins
-</programlisting>
-    </para>
-
-    <para>
-      Some plugins provide <link
-      linkend="module-services-discourse-site-settings">site
-      settings</link>. Their defaults can be configured using <xref
-      linkend="opt-services.discourse.siteSettings" />, just like
-      regular site settings. To find the names of these settings, look
-      in the <literal>config/settings.yml</literal> file of the plugin
-      repo.
-    </para>
-
-    <para>
-      For example, to add the <link
-      xlink:href="https://github.com/discourse/discourse-spoiler-alert">discourse-spoiler-alert</link>
-      and <link
-      xlink:href="https://github.com/discourse/discourse-solved">discourse-solved</link>
-      plugins, and disable <literal>discourse-spoiler-alert</literal>
-      by default:
-
-<programlisting>
-services.discourse = {
-  <link linkend="opt-services.discourse.enable">enable</link> = true;
-  <link linkend="opt-services.discourse.hostname">hostname</link> = "discourse.example.com";
-  <link linkend="opt-services.discourse.sslCertificate">sslCertificate</link> = "/path/to/ssl_certificate";
-  <link linkend="opt-services.discourse.sslCertificateKey">sslCertificateKey</link> = "/path/to/ssl_certificate_key";
-  admin = {
-    <link linkend="opt-services.discourse.admin.email">email</link> = "admin@example.com";
-    <link linkend="opt-services.discourse.admin.username">username</link> = "admin";
-    <link linkend="opt-services.discourse.admin.fullName">fullName</link> = "Administrator";
-    <link linkend="opt-services.discourse.admin.passwordFile">passwordFile</link> = "/path/to/password_file";
-  };
-  mail.outgoing = {
-    <link linkend="opt-services.discourse.mail.outgoing.serverAddress">serverAddress</link> = "smtp.emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.port">port</link> = 587;
-    <link linkend="opt-services.discourse.mail.outgoing.username">username</link> = "user@emailprovider.com";
-    <link linkend="opt-services.discourse.mail.outgoing.passwordFile">passwordFile</link> = "/path/to/smtp_password_file";
-  };
-  <link linkend="opt-services.discourse.mail.incoming.enable">mail.incoming.enable</link> = true;
-  <link linkend="opt-services.discourse.mail.incoming.enable">plugins</link> = with config.services.discourse.package.plugins; [
-    discourse-spoiler-alert
-    discourse-solved
-  ];
-  <link linkend="opt-services.discourse.siteSettings">siteSettings</link> = {
-    plugins = {
-      spoiler_enabled = false;
-    };
-  };
-  <link linkend="opt-services.discourse.secretKeyBaseFile">secretKeyBaseFile</link> = "/path/to/secret_key_base_file";
-};
-</programlisting>
-
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/dokuwiki.nix b/nixos/modules/services/web-apps/dokuwiki.nix
index f0b3c7b2bcf..3a66763b583 100644
--- a/nixos/modules/services/web-apps/dokuwiki.nix
+++ b/nixos/modules/services/web-apps/dokuwiki.nix
@@ -3,63 +3,184 @@
 with lib;
 
 let
+  inherit (lib.options) showOption showFiles;
+
   cfg = config.services.dokuwiki;
   eachSite = cfg.sites;
   user = "dokuwiki";
   webserver = config.services.${cfg.webserver};
 
-  dokuwikiAclAuthConfig = hostName: cfg: pkgs.writeText "acl.auth-${hostName}.php" ''
+  mkPhpIni = generators.toKeyValue {
+    mkKeyValue = generators.mkKeyValueDefault {} " = ";
+  };
+  mkPhpPackage = cfg: cfg.phpPackage.buildEnv {
+    extraConfig = mkPhpIni cfg.phpOptions;
+  };
+
+  dokuwikiAclAuthConfig = hostName: cfg: let
+    inherit (cfg) acl;
+    acl_gen = concatMapStringsSep "\n" (l: "${l.page} \t ${l.actor} \t ${toString l.level}");
+  in pkgs.writeText "acl.auth-${hostName}.php" ''
     # acl.auth.php
     # <?php exit()?>
     #
     # Access Control Lists
     #
-    ${toString cfg.acl}
+    ${if isString acl then acl else acl_gen acl}
   '';
 
-  dokuwikiLocalConfig = hostName: cfg: pkgs.writeText "local-${hostName}.php" ''
-    <?php
-    $conf['savedir'] = '${cfg.stateDir}';
-    $conf['superuser'] = '${toString cfg.superUser}';
-    $conf['useacl'] = '${toString cfg.aclUse}';
-    $conf['disableactions'] = '${cfg.disableActions}';
-    ${toString cfg.extraConfig}
+  mergeConfig = cfg: {
+    useacl = false; # Dokuwiki default
+    savedir = cfg.stateDir;
+  } // cfg.settings;
+
+  writePhpFile = name: text: pkgs.writeTextFile {
+    inherit name;
+    text = "<?php\n${text}";
+    checkPhase = "${pkgs.php81}/bin/php --syntax-check $target";
+  };
+
+  mkPhpValue = v: let
+    isHasAttr = s: isAttrs v && hasAttr s v;
+  in
+    if isString v then escapeShellArg v
+    # NOTE: If any value contains a , (comma) this will not get escaped
+    else if isList v && any lib.strings.isCoercibleToString v then escapeShellArg (concatMapStringsSep "," toString v)
+    else if isInt v then toString v
+    else if isBool v then toString (if v then 1 else 0)
+    else if isHasAttr "_file" then "trim(file_get_contents(${lib.escapeShellArg v._file}))"
+    else if isHasAttr "_raw" then v._raw
+    else abort "The dokuwiki localConf value ${lib.generators.toPretty {} v} can not be encoded."
+  ;
+
+  mkPhpAttrVals = v: flatten (mapAttrsToList mkPhpKeyVal v);
+  mkPhpKeyVal = k: v: let
+    values = if (isAttrs v && (hasAttr "_file" v || hasAttr "_raw" v )) || !isAttrs v then
+      [" = ${mkPhpValue v};"]
+    else
+      mkPhpAttrVals v;
+  in map (e: "[${escapeShellArg k}]${e}") (flatten values);
+
+  dokuwikiLocalConfig = hostName: cfg: let
+    conf_gen = c: map (v: "$conf${v}") (mkPhpAttrVals c);
+  in writePhpFile "local-${hostName}.php" ''
+    ${concatStringsSep "\n" (conf_gen cfg.mergedConfig)}
   '';
 
-  dokuwikiPluginsLocalConfig = hostName: cfg: pkgs.writeText "plugins.local-${hostName}.php" ''
-    <?php
-    ${cfg.pluginsConfig}
+  dokuwikiPluginsLocalConfig = hostName: cfg: let
+    pc = cfg.pluginsConfig;
+    pc_gen = pc: concatStringsSep "\n" (mapAttrsToList (n: v: "$plugins['${n}'] = ${boolToString v};") pc);
+  in writePhpFile "plugins.local-${hostName}.php" ''
+    ${if isString pc then pc else pc_gen pc}
   '';
 
 
-  pkg = hostName: cfg: pkgs.stdenv.mkDerivation rec {
-    pname = "dokuwiki-${hostName}";
-    version = src.version;
-    src = cfg.package;
+  pkg = hostName: cfg: cfg.package.combine {
+    inherit (cfg) plugins templates;
 
-    installPhase = ''
-      mkdir -p $out
-      cp -r * $out/
+    pname = p: "${p.pname}-${hostName}";
+
+    basePackage = cfg.package;
+    localConfig = dokuwikiLocalConfig hostName cfg;
+    pluginsConfig = dokuwikiPluginsLocalConfig hostName cfg;
+    aclConfig = if cfg.settings.useacl && cfg.acl != null then dokuwikiAclAuthConfig hostName cfg else null;
+  };
 
-      # symlink the dokuwiki config
-      ln -s ${dokuwikiLocalConfig hostName cfg} $out/share/dokuwiki/local.php
+  aclOpts = { ... }: {
+    options = {
 
-      # symlink plugins config
-      ln -s ${dokuwikiPluginsLocalConfig hostName cfg} $out/share/dokuwiki/plugins.local.php
+      page = mkOption {
+        type = types.str;
+        description = lib.mdDoc "Page or namespace to restrict";
+        example = "start";
+      };
 
-      # symlink acl
-      ln -s ${dokuwikiAclAuthConfig hostName cfg} $out/share/dokuwiki/acl.auth.php
+      actor = mkOption {
+        type = types.str;
+        description = lib.mdDoc "User or group to restrict";
+        example = "@external";
+      };
 
-      # symlink additional plugin(s) and templates(s)
-      ${concatMapStringsSep "\n" (template: "ln -s ${template} $out/share/dokuwiki/lib/tpl/${template.name}") cfg.templates}
-      ${concatMapStringsSep "\n" (plugin: "ln -s ${plugin} $out/share/dokuwiki/lib/plugins/${plugin.name}") cfg.plugins}
-    '';
+      level = let
+        available = {
+          "none" = 0;
+          "read" = 1;
+          "edit" = 2;
+          "create" = 4;
+          "upload" = 8;
+          "delete" = 16;
+        };
+      in mkOption {
+        type = types.enum ((attrValues available) ++ (attrNames available));
+        apply = x: if isInt x then x else available.${x};
+        description = lib.mdDoc ''
+          Permission level to restrict the actor(s) to.
+          See <https://www.dokuwiki.org/acl#background_info> for explanation
+        '';
+        example = "read";
+      };
+    };
   };
 
-  siteOpts = { config, lib, name, ... }:
+  # The current implementations of `doRename`,  `mkRenamedOptionModule` do not provide the full options path when used with submodules.
+  # They would only show `settings.useacl' instead of `services.dokuwiki.sites."site1.local".settings.useacl'
+  # The partial re-implementation of these functions is done to help users in debugging by showing the full path.
+  mkRenamed = from: to: { config, options, name, ... }: let
+    pathPrefix = [ "services" "dokuwiki" "sites" name ];
+    fromPath = pathPrefix  ++ from;
+    fromOpt = getAttrFromPath from options;
+    toOp = getAttrsFromPath to config;
+    toPath = pathPrefix ++ to;
+  in {
+    options = setAttrByPath from (mkOption {
+      visible = false;
+      description = lib.mdDoc "Alias of {option}${showOption toPath}";
+      apply = x: builtins.trace "Obsolete option `${showOption fromPath}' is used. It was renamed to ${showOption toPath}" toOp;
+    });
+    config = mkMerge [
+      {
+        warnings = optional fromOpt.isDefined
+          "The option `${showOption fromPath}' defined in ${showFiles fromOpt.files} has been renamed to `${showOption toPath}'.";
+      }
+      (lib.modules.mkAliasAndWrapDefsWithPriority (setAttrByPath to) fromOpt)
+    ];
+  };
+
+  siteOpts = { options, config, lib, name, ... }:
     {
+      imports = [
+        (mkRenamed [ "aclUse" ] [ "settings" "useacl" ])
+        (mkRenamed [ "superUser" ] [ "settings" "superuser" ])
+        (mkRenamed [ "disableActions" ] [ "settings"  "disableactions" ])
+        ({ config, options, ... }: let
+          showPath = suffix: lib.options.showOption ([ "services" "dokuwiki" "sites" name ] ++ suffix);
+          replaceExtraConfig = "Please use `${showPath ["settings"]}' to pass structured settings instead.";
+          ecOpt = options.extraConfig;
+          ecPath = showPath [ "extraConfig" ];
+        in {
+          options.extraConfig = mkOption {
+            visible = false;
+            apply = x: throw "The option ${ecPath} can no longer be used since it's been removed.\n${replaceExtraConfig}";
+          };
+          config.assertions = [
+            {
+              assertion = !ecOpt.isDefined;
+              message = "The option definition `${ecPath}' in ${showFiles ecOpt.files} no longer has any effect; please remove it.\n${replaceExtraConfig}";
+            }
+            {
+              assertion = config.mergedConfig.useacl -> (config.acl != null || config.aclFile != null);
+              message = "Either ${showPath [ "acl" ]} or ${showPath [ "aclFile" ]} is mandatory if ${showPath [ "settings" "useacl" ]} is true";
+            }
+            {
+              assertion = config.usersFile != null -> config.mergedConfig.useacl != false;
+              message = "${showPath [ "settings" "useacl" ]} is required when ${showPath [ "usersFile" ]} is set (Currently defiend as `${config.usersFile}' in ${showFiles options.usersFile.files}).";
+            }
+          ];
+        })
+      ];
+
       options = {
-        enable = mkEnableOption (lib.mdDoc "DokuWiki web application.");
+        enable = mkEnableOption (lib.mdDoc "DokuWiki web application");
 
         package = mkOption {
           type = types.package;
@@ -75,9 +196,22 @@ let
         };
 
         acl = mkOption {
-          type = types.nullOr types.lines;
+          type = with types; nullOr (listOf (submodule aclOpts));
           default = null;
-          example = "*               @ALL               8";
+          example = literalExpression ''
+            [
+              {
+                page = "start";
+                actor = "@external";
+                level = "read";
+              }
+              {
+                page = "*";
+                actor = "@users";
+                level = "upload";
+              }
+            ]
+          '';
           description = lib.mdDoc ''
             Access Control Lists: see <https://www.dokuwiki.org/acl>
             Mutually exclusive with services.dokuwiki.aclFile
@@ -90,7 +224,7 @@ let
 
         aclFile = mkOption {
           type = with types; nullOr str;
-          default = if (config.aclUse && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
+          default = if (config.mergedConfig.useacl && config.acl == null) then "/var/lib/dokuwiki/${name}/acl.auth.php" else null;
           description = lib.mdDoc ''
             Location of the dokuwiki acl rules. Mutually exclusive with services.dokuwiki.acl
             Mutually exclusive with services.dokuwiki.acl which is preferred.
@@ -100,42 +234,22 @@ let
           example = "/var/lib/dokuwiki/${name}/acl.auth.php";
         };
 
-        aclUse = mkOption {
-          type = types.bool;
-          default = true;
-          description = lib.mdDoc ''
-            Necessary for users to log in into the system.
-            Also limits anonymous users. When disabled,
-            everyone is able to create and edit content.
-          '';
-        };
-
         pluginsConfig = mkOption {
-          type = types.lines;
-          default = ''
-            $plugins['authad'] = 0;
-            $plugins['authldap'] = 0;
-            $plugins['authmysql'] = 0;
-            $plugins['authpgsql'] = 0;
-          '';
+          type = with types; attrsOf bool;
+          default = {
+            authad = false;
+            authldap = false;
+            authmysql = false;
+            authpgsql = false;
+          };
           description = lib.mdDoc ''
             List of the dokuwiki (un)loaded plugins.
           '';
         };
 
-        superUser = mkOption {
-          type = types.nullOr types.str;
-          default = "@admin";
-          description = lib.mdDoc ''
-            You can set either a username, a list of usernames (“admin1,admin2”),
-            or the name of a group by prepending an @ char to the groupname
-            Consult documentation <https://www.dokuwiki.org/config:superuser> for further instructions.
-          '';
-        };
-
         usersFile = mkOption {
           type = with types; nullOr str;
-          default = if config.aclUse then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
+          default = if config.mergedConfig.useacl then "/var/lib/dokuwiki/${name}/users.auth.php" else null;
           description = lib.mdDoc ''
             Location of the dokuwiki users file. List of users. Format:
 
@@ -150,17 +264,6 @@ let
           example = "/var/lib/dokuwiki/${name}/users.auth.php";
         };
 
-        disableActions = mkOption {
-          type = types.nullOr types.str;
-          default = "";
-          example = "search,register";
-          description = lib.mdDoc ''
-            Disable individual action modes. Refer to
-            <https://www.dokuwiki.org/config:action_modes>
-            for details on supported values.
-          '';
-        };
-
         plugins = mkOption {
           type = types.listOf types.path;
           default = [];
@@ -173,18 +276,14 @@ let
           '';
           example = literalExpression ''
                 let
-                  # Let's package the icalevents plugin
-                  plugin-icalevents = pkgs.stdenv.mkDerivation {
+                  plugin-icalevents = pkgs.stdenv.mkDerivation rec {
                     name = "icalevents";
-                    # Download the plugin from the dokuwiki site
-                    src = pkgs.fetchurl {
-                      url = "https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/2017-06-16/dokuwiki-plugin-icalevents-2017-06-16.zip";
-                      sha256 = "e40ed7dd6bbe7fe3363bbbecb4de481d5e42385b5a0f62f6a6ce6bf3a1f9dfa8";
+                    version = "2017-06-16";
+                    src = pkgs.fetchzip {
+                      stripRoot = false;
+                      url = "https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/''${version}/dokuwiki-plugin-icalevents-''${version}.zip";
+                      hash = "sha256-IPs4+qgEfe8AAWevbcCM9PnyI0uoyamtWeg4rEb+9Wc=";
                     };
-                    sourceRoot = ".";
-                    # We need unzip to build this package
-                    buildInputs = [ pkgs.unzip ];
-                    # Installing simply means copying all files to the output directory
                     installPhase = "mkdir -p $out; cp -R * $out/";
                   };
                 # And then pass this theme to the plugin list like this:
@@ -204,19 +303,17 @@ let
           '';
           example = literalExpression ''
                 let
-                  # Let's package the bootstrap3 theme
-                  template-bootstrap3 = pkgs.stdenv.mkDerivation {
-                    name = "bootstrap3";
-                    # Download the theme from the dokuwiki site
-                    src = pkgs.fetchurl {
-                      url = "https://github.com/giterlizzi/dokuwiki-template-bootstrap3/archive/v2019-05-22.zip";
-                      sha256 = "4de5ff31d54dd61bbccaf092c9e74c1af3a4c53e07aa59f60457a8f00cfb23a6";
-                    };
-                    # We need unzip to build this package
-                    buildInputs = [ pkgs.unzip ];
-                    # Installing simply means copying all files to the output directory
-                    installPhase = "mkdir -p $out; cp -R * $out/";
+                  template-bootstrap3 = pkgs.stdenv.mkDerivation rec {
+                  name = "bootstrap3";
+                  version = "2022-07-27";
+                  src = pkgs.fetchFromGitHub {
+                    owner = "giterlizzi";
+                    repo = "dokuwiki-template-bootstrap3";
+                    rev = "v''${version}";
+                    hash = "sha256-B3Yd4lxdwqfCnfmZdp+i/Mzwn/aEuZ0ovagDxuR6lxo=";
                   };
+                  installPhase = "mkdir -p $out; cp -R * $out/";
+                };
                 # And then pass this theme to the template list like this:
                 in [ template-bootstrap3 ]
           '';
@@ -238,26 +335,92 @@ let
           '';
         };
 
-        extraConfig = mkOption {
-          type = types.nullOr types.lines;
-          default = null;
-          example = ''
-            $conf['title'] = 'My Wiki';
-            $conf['userewrite'] = 1;
+        phpPackage = mkOption {
+          type = types.package;
+          relatedPackages = [ "php80" "php81" ];
+          default = pkgs.php81;
+          defaultText = "pkgs.php81";
+          description = lib.mdDoc ''
+            PHP package to use for this dokuwiki site.
           '';
+        };
+
+        phpOptions = mkOption {
+          type = types.attrsOf types.str;
+          default = {};
           description = lib.mdDoc ''
-            DokuWiki configuration. Refer to
-            <https://www.dokuwiki.org/config>
-            for details on supported values.
+            Options for PHP's php.ini file for this dokuwiki site.
+          '';
+          example = literalExpression ''
+          {
+            "opcache.interned_strings_buffer" = "8";
+            "opcache.max_accelerated_files" = "10000";
+            "opcache.memory_consumption" = "128";
+            "opcache.revalidate_freq" = "15";
+            "opcache.fast_shutdown" = "1";
+          }
           '';
         };
 
-      };
+        settings = mkOption {
+          type = types.attrsOf types.anything;
+          default = {
+            useacl = true;
+            superuser = "admin";
+          };
+          description = lib.mdDoc ''
+            Structural DokuWiki configuration.
+            Refer to <https://www.dokuwiki.org/config>
+            for details and supported values.
+            Settings can either be directly set from nix,
+            loaded from a file using `._file` or obtained from any
+            PHP function calls using `._raw`.
+          '';
+          example = literalExpression ''
+            {
+              title = "My Wiki";
+              userewrite = 1;
+              disableactions = [ "register" ]; # Will be concatenated with commas
+              plugin.smtp = {
+                smtp_pass._file = "/var/run/secrets/dokuwiki/smtp_pass";
+                smtp_user._raw = "getenv('DOKUWIKI_SMTP_USER')";
+              };
+            }
+          '';
+        };
 
+        mergedConfig = mkOption {
+          readOnly = true;
+          default = mergeConfig config;
+          defaultText = literalExpression ''
+            {
+              useacl = true;
+            }
+          '';
+          description = lib.mdDoc ''
+            Read only representation of the final configuration.
+          '';
+        };
+
+      # Required for the mkRenamedOptionModule
+      # TODO: Remove me once https://github.com/NixOS/nixpkgs/issues/96006 is fixed
+      # or we don't have any more notes about the removal of extraConfig, ...
+      warnings = mkOption {
+        type = types.listOf types.unspecified;
+        default = [ ];
+        visible = false;
+        internal = true;
+      };
+      assertions = mkOption {
+        type = types.listOf types.unspecified;
+        default = [ ];
+        visible = false;
+        internal = true;
+      };
     };
+  };
 in
 {
-  # interface
   options = {
     services.dokuwiki = {
 
@@ -276,8 +439,8 @@ in
           Further nginx configuration can be done by adapting `services.nginx.virtualHosts.<name>`.
           See [](#opt-services.nginx.virtualHosts) for further information.
 
-          Further apache2 configuration can be done by adapting `services.httpd.virtualHosts.<name>`.
-          See [](#opt-services.httpd.virtualHosts) for further information.
+          Further caddy configuration can be done by adapting `services.caddy.virtualHosts.<name>`.
+          See [](#opt-services.caddy.virtualHosts) for further information.
         '';
       };
 
@@ -287,29 +450,19 @@ in
   # implementation
   config = mkIf (eachSite != {}) (mkMerge [{
 
-    assertions = flatten (mapAttrsToList (hostName: cfg:
-    [{
-      assertion = cfg.aclUse -> (cfg.acl != null || cfg.aclFile != null);
-      message = "Either services.dokuwiki.sites.${hostName}.acl or services.dokuwiki.sites.${hostName}.aclFile is mandatory if aclUse true";
-    }
-    {
-      assertion = cfg.usersFile != null -> cfg.aclUse != false;
-      message = "services.dokuwiki.sites.${hostName}.aclUse must must be true if usersFile is not null";
-    }
-    ]) eachSite);
+    warnings = flatten (mapAttrsToList (_: cfg: cfg.warnings) eachSite);
+
+    assertions = flatten (mapAttrsToList (_: cfg: cfg.assertions) eachSite);
 
     services.phpfpm.pools = mapAttrs' (hostName: cfg: (
       nameValuePair "dokuwiki-${hostName}" {
         inherit user;
         group = webserver.group;
 
-        phpPackage = pkgs.php81;
-        phpEnv = {
-          DOKUWIKI_LOCAL_CONFIG = "${dokuwikiLocalConfig hostName cfg}";
-          DOKUWIKI_PLUGINS_LOCAL_CONFIG = "${dokuwikiPluginsLocalConfig hostName cfg}";
-        } // optionalAttrs (cfg.usersFile != null) {
+        phpPackage = mkPhpPackage cfg;
+        phpEnv = optionalAttrs (cfg.usersFile != null) {
           DOKUWIKI_USERS_AUTH_CONFIG = "${cfg.usersFile}";
-        } //optionalAttrs (cfg.aclUse) {
+        } // optionalAttrs (cfg.mergedConfig.useacl) {
           DOKUWIKI_ACL_AUTH_CONFIG = if (cfg.acl != null) then "${dokuwikiAclAuthConfig hostName cfg}" else "${toString cfg.aclFile}";
         };
 
@@ -444,5 +597,6 @@ in
     _1000101
     onny
     dandellion
+    e1mo
   ];
 }
diff --git a/nixos/modules/services/web-apps/dolibarr.nix b/nixos/modules/services/web-apps/dolibarr.nix
index 5335c439329..453229c130c 100644
--- a/nixos/modules/services/web-apps/dolibarr.nix
+++ b/nixos/modules/services/web-apps/dolibarr.nix
@@ -1,11 +1,11 @@
 { config, pkgs, lib, ... }:
 let
-  inherit (lib) any boolToString concatStringsSep isBool isString literalExpression mapAttrsToList mkDefault mkEnableOption mkIf mkOption optionalAttrs types;
+  inherit (lib) any boolToString concatStringsSep isBool isString mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption optionalAttrs types;
 
   package = pkgs.dolibarr.override { inherit (cfg) stateDir; };
 
   cfg = config.services.dolibarr;
-  vhostCfg = config.services.nginx.virtualHosts."${cfg.domain}";
+  vhostCfg = lib.optionalAttrs (cfg.nginx != null) config.services.nginx.virtualHosts."${cfg.domain}";
 
   mkConfigFile = filename: settings:
     let
@@ -16,7 +16,7 @@ let
         if (any (str: k == str) secretKeys) then v
         else if isString v then "'${v}'"
         else if isBool v then boolToString v
-        else if isNull v then "null"
+        else if v == null then "null"
         else toString v
       ;
     in
@@ -38,7 +38,7 @@ let
     force_install_database = cfg.database.name;
     force_install_databaselogin = cfg.database.user;
 
-    force_install_mainforcehttps = vhostCfg.forceSSL;
+    force_install_mainforcehttps = vhostCfg.forceSSL or false;
     force_install_createuser = false;
     force_install_dolibarrlogin = null;
   } // optionalAttrs (cfg.database.passwordFile != null) {
@@ -183,7 +183,8 @@ in
   };
 
   # implementation
-  config = mkIf cfg.enable {
+  config = mkIf cfg.enable (mkMerge [
+    {
 
     assertions = [
       { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
@@ -214,7 +215,7 @@ in
 
       # Security settings
       dolibarr_main_prod = true;
-      dolibarr_main_force_https = vhostCfg.forceSSL;
+      dolibarr_main_force_https = vhostCfg.forceSSL or false;
       dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql";
       dolibarr_nocsrfcheck = false;
       dolibarr_main_instance_unique_id = ''
@@ -314,7 +315,9 @@ in
     users.groups = optionalAttrs (cfg.group == "dolibarr") {
       dolibarr = { };
     };
-
-    users.users."${config.services.nginx.group}".extraGroups = [ cfg.group ];
-  };
+  }
+  (mkIf (cfg.nginx != null) {
+    users.users."${config.services.nginx.group}".extraGroups = mkIf (cfg.nginx != null) [ cfg.group ];
+  })
+]);
 }
diff --git a/nixos/modules/services/web-apps/freshrss.nix b/nixos/modules/services/web-apps/freshrss.nix
index c05e7b2c4f7..89e29f7ccb5 100644
--- a/nixos/modules/services/web-apps/freshrss.nix
+++ b/nixos/modules/services/web-apps/freshrss.nix
@@ -60,7 +60,7 @@ in
       };
 
       port = mkOption {
-        type = with types; nullOr port;
+        type = types.nullOr types.port;
         default = null;
         description = mdDoc "Database port for FreshRSS.";
         example = 3306;
@@ -73,7 +73,7 @@ in
       };
 
       passFile = mkOption {
-        type = types.nullOr types.str;
+        type = types.nullOr types.path;
         default = null;
         description = mdDoc "Database password file for FreshRSS.";
         example = "/run/secrets/freshrss";
@@ -116,12 +116,18 @@ in
         with default values.
       '';
     };
-  };
 
+    user = mkOption {
+      type = types.str;
+      default = "freshrss";
+      description = lib.mdDoc "User under which Freshrss runs.";
+    };
+  };
 
   config =
     let
-      systemd-hardening = {
+      defaultServiceConfig = {
+        ReadWritePaths = "${cfg.dataDir}";
         CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
         DeviceAllow = "";
         LockPersonality = true;
@@ -146,6 +152,11 @@ in
         SystemCallArchitectures = "native";
         SystemCallFilter = [ "@system-service" "~@resources" "~@privileged" ];
         UMask = "0007";
+        Type = "oneshot";
+        User = cfg.user;
+        Group = config.users.users.${cfg.user}.group;
+        StateDirectory = "freshrss";
+        WorkingDirectory = cfg.package;
       };
     in
     mkIf cfg.enable {
@@ -199,12 +210,17 @@ in
         };
       };
 
-      users.users.freshrss = {
+      users.users."${cfg.user}" = {
         description = "FreshRSS service user";
         isSystemUser = true;
-        group = "freshrss";
+        group = "${cfg.user}";
+        home = cfg.dataDir;
       };
-      users.groups.freshrss = { };
+      users.groups."${cfg.user}" = { };
+
+      systemd.tmpfiles.rules = [
+        "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
+      ];
 
       systemd.services.freshrss-config =
         let
@@ -228,30 +244,24 @@ in
         {
           description = "Set up the state directory for FreshRSS before use";
           wantedBy = [ "multi-user.target" ];
-          serviceConfig = {
+          serviceConfig = defaultServiceConfig //{
             Type = "oneshot";
             User = "freshrss";
             Group = "freshrss";
             StateDirectory = "freshrss";
             WorkingDirectory = cfg.package;
-          } // systemd-hardening;
+          };
           environment = {
             FRESHRSS_DATA_PATH = cfg.dataDir;
           };
 
           script = ''
-            # create files with correct permissions
-            mkdir -m 755 -p ${cfg.dataDir}
-
             # do installation or reconfigure
             if test -f ${cfg.dataDir}/config.php; then
               # reconfigure with settings
               ./cli/reconfigure.php ${settingsFlags}
               ./cli/update-user.php --user ${cfg.defaultUser} --password "$(cat ${cfg.passwordFile})"
             else
-              # Copy the user data template directory
-              cp -r ./data ${cfg.dataDir}
-
               # check correct folders in data folder
               ./cli/prepare.php
               # install with settings
@@ -269,14 +279,9 @@ in
         environment = {
           FRESHRSS_DATA_PATH = cfg.dataDir;
         };
-        serviceConfig = {
-          Type = "oneshot";
-          User = "freshrss";
-          Group = "freshrss";
-          StateDirectory = "freshrss";
-          WorkingDirectory = cfg.package;
+        serviceConfig = defaultServiceConfig //{
           ExecStart = "${cfg.package}/app/actualize_script.php";
-        } // systemd-hardening;
+        };
       };
     };
 }
diff --git a/nixos/modules/services/web-apps/galene.nix b/nixos/modules/services/web-apps/galene.nix
index 15ef09aa0b8..747b85f94c6 100644
--- a/nixos/modules/services/web-apps/galene.nix
+++ b/nixos/modules/services/web-apps/galene.nix
@@ -12,7 +12,7 @@ in
 {
   options = {
     services.galene = {
-      enable = mkEnableOption (lib.mdDoc "Galene Service.");
+      enable = mkEnableOption (lib.mdDoc "Galene Service");
 
       stateDir = mkOption {
         default = defaultstateDir;
diff --git a/nixos/modules/services/web-apps/grocy.md b/nixos/modules/services/web-apps/grocy.md
new file mode 100644
index 00000000000..62aad4b103d
--- /dev/null
+++ b/nixos/modules/services/web-apps/grocy.md
@@ -0,0 +1,66 @@
+# Grocy {#module-services-grocy}
+
+[Grocy](https://grocy.info/) is a web-based self-hosted groceries
+& household management solution for your home.
+
+## Basic usage {#module-services-grocy-basic-usage}
+
+A very basic configuration may look like this:
+```
+{ pkgs, ... }:
+{
+  services.grocy = {
+    enable = true;
+    hostName = "grocy.tld";
+  };
+}
+```
+This configures a simple vhost using [nginx](#opt-services.nginx.enable)
+which listens to `grocy.tld` with fully configured ACME/LE (this can be
+disabled by setting [services.grocy.nginx.enableSSL](#opt-services.grocy.nginx.enableSSL)
+to `false`). After the initial setup the credentials `admin:admin`
+can be used to login.
+
+The application's state is persisted at `/var/lib/grocy/grocy.db` in a
+`sqlite3` database. The migration is applied when requesting the `/`-route
+of the application.
+
+## Settings {#module-services-grocy-settings}
+
+The configuration for `grocy` is located at `/etc/grocy/config.php`.
+By default, the following settings can be defined in the NixOS-configuration:
+```
+{ pkgs, ... }:
+{
+  services.grocy.settings = {
+    # The default currency in the system for invoices etc.
+    # Please note that exchange rates aren't taken into account, this
+    # is just the setting for what's shown in the frontend.
+    currency = "EUR";
+
+    # The display language (and locale configuration) for grocy.
+    culture = "de";
+
+    calendar = {
+      # Whether or not to show the week-numbers
+      # in the calendar.
+      showWeekNumber = true;
+
+      # Index of the first day to be shown in the calendar (0=Sunday, 1=Monday,
+      # 2=Tuesday and so on).
+      firstDayOfWeek = 2;
+    };
+  };
+}
+```
+
+If you want to alter the configuration file on your own, you can do this manually with
+an expression like this:
+```
+{ lib, ... }:
+{
+  environment.etc."grocy/config.php".text = lib.mkAfter ''
+    // Arbitrary PHP code in grocy's configuration file
+  '';
+}
+```
diff --git a/nixos/modules/services/web-apps/grocy.nix b/nixos/modules/services/web-apps/grocy.nix
index 6efc2ccfd30..3bcda3caeda 100644
--- a/nixos/modules/services/web-apps/grocy.nix
+++ b/nixos/modules/services/web-apps/grocy.nix
@@ -167,6 +167,6 @@ in {
 
   meta = {
     maintainers = with maintainers; [ ma27 ];
-    doc = ./grocy.xml;
+    doc = ./grocy.md;
   };
 }
diff --git a/nixos/modules/services/web-apps/grocy.xml b/nixos/modules/services/web-apps/grocy.xml
deleted file mode 100644
index fdf6d00f4b1..00000000000
--- a/nixos/modules/services/web-apps/grocy.xml
+++ /dev/null
@@ -1,77 +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-grocy">
-
-  <title>Grocy</title>
-  <para>
-    <link xlink:href="https://grocy.info/">Grocy</link> is a web-based self-hosted groceries
-    &amp; household management solution for your home.
-  </para>
-
-  <section xml:id="module-services-grocy-basic-usage">
-   <title>Basic usage</title>
-   <para>
-    A very basic configuration may look like this:
-<programlisting>{ pkgs, ... }:
-{
-  services.grocy = {
-    <link linkend="opt-services.grocy.enable">enable</link> = true;
-    <link linkend="opt-services.grocy.hostName">hostName</link> = "grocy.tld";
-  };
-}</programlisting>
-    This configures a simple vhost using <link linkend="opt-services.nginx.enable">nginx</link>
-    which listens to <literal>grocy.tld</literal> with fully configured ACME/LE (this can be
-    disabled by setting <link linkend="opt-services.grocy.nginx.enableSSL">services.grocy.nginx.enableSSL</link>
-    to <literal>false</literal>). After the initial setup the credentials <literal>admin:admin</literal>
-    can be used to login.
-   </para>
-   <para>
-    The application's state is persisted at <literal>/var/lib/grocy/grocy.db</literal> in a
-    <package>sqlite3</package> database. The migration is applied when requesting the <literal>/</literal>-route
-    of the application.
-   </para>
-  </section>
-
-  <section xml:id="module-services-grocy-settings">
-   <title>Settings</title>
-   <para>
-    The configuration for <literal>grocy</literal> is located at <literal>/etc/grocy/config.php</literal>.
-    By default, the following settings can be defined in the NixOS-configuration:
-<programlisting>{ pkgs, ... }:
-{
-  services.grocy.settings = {
-    # The default currency in the system for invoices etc.
-    # Please note that exchange rates aren't taken into account, this
-    # is just the setting for what's shown in the frontend.
-    <link linkend="opt-services.grocy.settings.currency">currency</link> = "EUR";
-
-    # The display language (and locale configuration) for grocy.
-    <link linkend="opt-services.grocy.settings.currency">culture</link> = "de";
-
-    calendar = {
-      # Whether or not to show the week-numbers
-      # in the calendar.
-      <link linkend="opt-services.grocy.settings.calendar.showWeekNumber">showWeekNumber</link> = true;
-
-      # Index of the first day to be shown in the calendar (0=Sunday, 1=Monday,
-      # 2=Tuesday and so on).
-      <link linkend="opt-services.grocy.settings.calendar.firstDayOfWeek">firstDayOfWeek</link> = 2;
-    };
-  };
-}</programlisting>
-   </para>
-   <para>
-    If you want to alter the configuration file on your own, you can do this manually with
-    an expression like this:
-<programlisting>{ lib, ... }:
-{
-  environment.etc."grocy/config.php".text = lib.mkAfter ''
-    // Arbitrary PHP code in grocy's configuration file
-  '';
-}</programlisting>
-   </para>
-  </section>
-
-</chapter>
diff --git a/nixos/modules/services/web-apps/hedgedoc.nix b/nixos/modules/services/web-apps/hedgedoc.nix
index a623e45691d..a7823354ce8 100644
--- a/nixos/modules/services/web-apps/hedgedoc.nix
+++ b/nixos/modules/services/web-apps/hedgedoc.nix
@@ -291,7 +291,8 @@ in
       };
       defaultNotePath = mkOption {
         type = types.nullOr types.str;
-        default = "./public/default.md";
+        default = "${cfg.package}/public/default.md";
+        defaultText = literalExpression "\"\${cfg.package}/public/default.md\"";
         description = lib.mdDoc ''
           Path to the default Note file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
@@ -299,7 +300,8 @@ in
       };
       docsPath = mkOption {
         type = types.nullOr types.str;
-        default = "./public/docs";
+        default = "${cfg.package}/public/docs";
+        defaultText = literalExpression "\"\${cfg.package}/public/docs\"";
         description = lib.mdDoc ''
           Path to the docs directory.
           (Non-canonical paths are relative to HedgeDoc's base directory)
@@ -307,7 +309,8 @@ in
       };
       indexPath = mkOption {
         type = types.nullOr types.str;
-        default = "./public/views/index.ejs";
+        default = "${cfg.package}/public/views/index.ejs";
+        defaultText = literalExpression "\"\${cfg.package}/public/views/index.ejs\"";
         description = lib.mdDoc ''
           Path to the index template file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
@@ -315,7 +318,8 @@ in
       };
       hackmdPath = mkOption {
         type = types.nullOr types.str;
-        default = "./public/views/hackmd.ejs";
+        default = "${cfg.package}/public/views/hackmd.ejs";
+        defaultText = literalExpression "\"\${cfg.package}/public/views/hackmd.ejs\"";
         description = lib.mdDoc ''
           Path to the hackmd template file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
@@ -323,8 +327,8 @@ in
       };
       errorPath = mkOption {
         type = types.nullOr types.str;
-        default = null;
-        defaultText = literalExpression "./public/views/error.ejs";
+        default = "${cfg.package}/public/views/error.ejs";
+        defaultText = literalExpression "\"\${cfg.package}/public/views/error.ejs\"";
         description = lib.mdDoc ''
           Path to the error template file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
@@ -332,8 +336,8 @@ in
       };
       prettyPath = mkOption {
         type = types.nullOr types.str;
-        default = null;
-        defaultText = literalExpression "./public/views/pretty.ejs";
+        default = "${cfg.package}/public/views/pretty.ejs";
+        defaultText = literalExpression "\"\${cfg.package}/public/views/pretty.ejs\"";
         description = lib.mdDoc ''
           Path to the pretty template file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
@@ -341,8 +345,8 @@ in
       };
       slidePath = mkOption {
         type = types.nullOr types.str;
-        default = null;
-        defaultText = literalExpression "./public/views/slide.hbs";
+        default = "${cfg.package}/public/views/slide.hbs";
+        defaultText = literalExpression "\"\${cfg.package}/public/views/slide.hbs\"";
         description = lib.mdDoc ''
           Path to the slide template file.
           (Non-canonical paths are relative to HedgeDoc's base directory)
@@ -351,7 +355,7 @@ in
       uploadsPath = mkOption {
         type = types.str;
         default = "${cfg.workDir}/uploads";
-        defaultText = literalExpression "/var/lib/${name}/uploads";
+        defaultText = literalExpression "\"\${cfg.workDir}/uploads\"";
         description = lib.mdDoc ''
           Path under which uploaded files are saved.
         '';
@@ -946,16 +950,16 @@ in
                 type = types.str;
                 default = "";
                 description = lib.mdDoc ''
-                  Attribute map for `id'.
-                  Defaults to `NameID' of SAML response.
+                  Attribute map for `id`.
+                  Defaults to `NameID` of SAML response.
                 '';
               };
               username = mkOption {
                 type = types.str;
                 default = "";
                 description = lib.mdDoc ''
-                  Attribute map for `username'.
-                  Defaults to `NameID' of SAML response.
+                  Attribute map for `username`.
+                  Defaults to `NameID` of SAML response.
                 '';
               };
               email = mkOption {
diff --git a/nixos/modules/services/web-apps/hledger-web.nix b/nixos/modules/services/web-apps/hledger-web.nix
index 86716a02649..0fc283ff521 100644
--- a/nixos/modules/services/web-apps/hledger-web.nix
+++ b/nixos/modules/services/web-apps/hledger-web.nix
@@ -7,7 +7,7 @@ in {
 
     enable = mkEnableOption (lib.mdDoc "hledger-web service");
 
-    serveApi = mkEnableOption (lib.mdDoc "Serve only the JSON web API, without the web UI.");
+    serveApi = mkEnableOption (lib.mdDoc "Serve only the JSON web API, without the web UI");
 
     host = mkOption {
       type = types.str;
diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix
deleted file mode 100644
index a61aa445f82..00000000000
--- a/nixos/modules/services/web-apps/ihatemoney/default.nix
+++ /dev/null
@@ -1,153 +0,0 @@
-{ config, pkgs, lib, ... }:
-with lib;
-let
-  cfg = config.services.ihatemoney;
-  user = "ihatemoney";
-  group = "ihatemoney";
-  db = "ihatemoney";
-  python3 = config.services.uwsgi.package.python3;
-  pkg = python3.pkgs.ihatemoney;
-  toBool = x: if x then "True" else "False";
-  configFile = pkgs.writeText "ihatemoney.cfg" ''
-        from secrets import token_hex
-        # load a persistent secret key
-        SECRET_KEY_FILE = "/var/lib/ihatemoney/secret_key"
-        SECRET_KEY = ""
-        try:
-          with open(SECRET_KEY_FILE) as f:
-            SECRET_KEY = f.read()
-        except FileNotFoundError:
-          pass
-        if not SECRET_KEY:
-          print("ihatemoney: generating a new secret key")
-          SECRET_KEY = token_hex(50)
-          with open(SECRET_KEY_FILE, "w") as f:
-            f.write(SECRET_KEY)
-        del token_hex
-        del SECRET_KEY_FILE
-
-        # "normal" configuration
-        DEBUG = False
-        SQLALCHEMY_DATABASE_URI = '${
-          if cfg.backend == "sqlite"
-          then "sqlite:////var/lib/ihatemoney/ihatemoney.sqlite"
-          else "postgresql:///${db}"}'
-        SQLALCHEMY_TRACK_MODIFICATIONS = False
-        MAIL_DEFAULT_SENDER = (r"${cfg.defaultSender.name}", r"${cfg.defaultSender.email}")
-        ACTIVATE_DEMO_PROJECT = ${toBool cfg.enableDemoProject}
-        ADMIN_PASSWORD = r"${toString cfg.adminHashedPassword /*toString null == ""*/}"
-        ALLOW_PUBLIC_PROJECT_CREATION = ${toBool cfg.enablePublicProjectCreation}
-        ACTIVATE_ADMIN_DASHBOARD = ${toBool cfg.enableAdminDashboard}
-        SESSION_COOKIE_SECURE = ${toBool cfg.secureCookie}
-        ENABLE_CAPTCHA = ${toBool cfg.enableCaptcha}
-        LEGAL_LINK = r"${toString cfg.legalLink}"
-
-        ${cfg.extraConfig}
-  '';
-in
-  {
-    options.services.ihatemoney = {
-      enable = mkEnableOption (lib.mdDoc "ihatemoney webapp. Note that this will set uwsgi to emperor mode");
-      backend = mkOption {
-        type = types.enum [ "sqlite" "postgresql" ];
-        default = "sqlite";
-        description = lib.mdDoc ''
-          The database engine to use for ihatemoney.
-          If `postgresql` is selected, then a database called
-          `${db}` will be created. If you disable this option,
-          it will however not be removed.
-        '';
-      };
-      adminHashedPassword = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc "The hashed password of the administrator. To obtain it, run `ihatemoney generate_password_hash`";
-      };
-      uwsgiConfig = mkOption {
-        type = types.attrs;
-        example = {
-          http = ":8000";
-        };
-        description = lib.mdDoc "Additional configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen.";
-      };
-      defaultSender = {
-        name = mkOption {
-          type = types.str;
-          default = "Budget manager";
-          description = lib.mdDoc "The display name of the sender of ihatemoney emails";
-        };
-        email = mkOption {
-          type = types.str;
-          default = "ihatemoney@${config.networking.hostName}";
-          defaultText = literalExpression ''"ihatemoney@''${config.networking.hostName}"'';
-          description = lib.mdDoc "The email of the sender of ihatemoney emails";
-        };
-      };
-      secureCookie = mkOption {
-        type = types.bool;
-        default = true;
-        description = lib.mdDoc "Use secure cookies. Disable this when ihatemoney is served via http instead of https";
-      };
-      enableDemoProject = mkEnableOption (lib.mdDoc "access to the demo project in ihatemoney");
-      enablePublicProjectCreation = mkEnableOption (lib.mdDoc "permission to create projects in ihatemoney by anyone");
-      enableAdminDashboard = mkEnableOption (lib.mdDoc "ihatemoney admin dashboard");
-      enableCaptcha = mkEnableOption (lib.mdDoc "a simplistic captcha for some forms");
-      legalLink = mkOption {
-        type = types.nullOr types.str;
-        default = null;
-        description = lib.mdDoc "The URL to a page explaining legal statements about your service, eg. GDPR-related information.";
-      };
-      extraConfig = mkOption {
-        type = types.str;
-        default = "";
-        description = lib.mdDoc "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation.";
-      };
-    };
-    config = mkIf cfg.enable {
-      services.postgresql = mkIf (cfg.backend == "postgresql") {
-        enable = true;
-        ensureDatabases = [ db ];
-        ensureUsers = [ {
-          name = user;
-          ensurePermissions = {
-            "DATABASE ${db}" = "ALL PRIVILEGES";
-          };
-        } ];
-      };
-      systemd.services.postgresql = mkIf (cfg.backend == "postgresql") {
-        wantedBy = [ "uwsgi.service" ];
-        before = [ "uwsgi.service" ];
-      };
-      systemd.tmpfiles.rules = [
-        "d /var/lib/ihatemoney 770 ${user} ${group}"
-      ];
-      users = {
-        users.${user} = {
-          isSystemUser = true;
-          inherit group;
-        };
-        groups.${group} = {};
-      };
-      services.uwsgi = {
-        enable = true;
-        plugins = [ "python3" ];
-        instance = {
-          type = "emperor";
-          vassals.ihatemoney = {
-            type = "normal";
-            strict = true;
-            immediate-uid = user;
-            immediate-gid = group;
-            # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c
-            enable-threads = true;
-            module = "wsgi:application";
-            chdir = "${pkg}/${pkg.pythonModule.sitePackages}/ihatemoney";
-            env = [ "IHATEMONEY_SETTINGS_FILE_PATH=${configFile}" ];
-            pythonPackages = self: [ self.ihatemoney ];
-          } // cfg.uwsgiConfig;
-        };
-      };
-    };
-  }
-
-
diff --git a/nixos/modules/services/web-apps/jirafeau.nix b/nixos/modules/services/web-apps/jirafeau.nix
index 293cbb3af42..b2e27416716 100644
--- a/nixos/modules/services/web-apps/jirafeau.nix
+++ b/nixos/modules/services/web-apps/jirafeau.nix
@@ -36,7 +36,7 @@ in
       description = lib.mdDoc "Location of Jirafeau storage directory.";
     };
 
-    enable = mkEnableOption (lib.mdDoc "Jirafeau file upload application.");
+    enable = mkEnableOption (lib.mdDoc "Jirafeau file upload application");
 
     extraConfig = mkOption {
       type = types.lines;
diff --git a/nixos/modules/services/web-apps/jitsi-meet.md b/nixos/modules/services/web-apps/jitsi-meet.md
new file mode 100644
index 00000000000..060ef975265
--- /dev/null
+++ b/nixos/modules/services/web-apps/jitsi-meet.md
@@ -0,0 +1,45 @@
+# Jitsi Meet {#module-services-jitsi-meet}
+
+With Jitsi Meet on NixOS you can quickly configure a complete,
+private, self-hosted video conferencing solution.
+
+## Basic usage {#module-services-jitsi-basic-usage}
+
+A minimal configuration using Let's Encrypt for TLS certificates looks like this:
+```
+{
+  services.jitsi-meet = {
+    enable = true;
+    hostName = "jitsi.example.com";
+  };
+  services.jitsi-videobridge.openFirewall = true;
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+  security.acme.email = "me@example.com";
+  security.acme.acceptTerms = true;
+}
+```
+
+## Configuration {#module-services-jitsi-configuration}
+
+Here is the minimal configuration with additional configurations:
+```
+{
+  services.jitsi-meet = {
+    enable = true;
+    hostName = "jitsi.example.com";
+    config = {
+      enableWelcomePage = false;
+      prejoinPageEnabled = true;
+      defaultLang = "fi";
+    };
+    interfaceConfig = {
+      SHOW_JITSI_WATERMARK = false;
+      SHOW_WATERMARK_FOR_GUESTS = false;
+    };
+  };
+  services.jitsi-videobridge.openFirewall = true;
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+  security.acme.email = "me@example.com";
+  security.acme.acceptTerms = true;
+}
+```
diff --git a/nixos/modules/services/web-apps/jitsi-meet.nix b/nixos/modules/services/web-apps/jitsi-meet.nix
index 5b0934b2fb7..3825b03c244 100644
--- a/nixos/modules/services/web-apps/jitsi-meet.nix
+++ b/nixos/modules/services/web-apps/jitsi-meet.nix
@@ -411,11 +411,14 @@ in
       componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
       bridgeMuc = "jvbbrewery@internal.${cfg.hostName}";
       config = mkMerge [{
-        "org.jitsi.jicofo.ALWAYS_TRUST_MODE_ENABLED" = "true";
+        jicofo.xmpp.service.disable-certificate-verification = true;
+        jicofo.xmpp.client.disable-certificate-verification = true;
       #} (lib.mkIf cfg.jibri.enable {
        } (lib.mkIf (config.services.jibri.enable || cfg.jibri.enable) {
-        "org.jitsi.jicofo.jibri.BREWERY" = "JibriBrewery@internal.${cfg.hostName}";
-        "org.jitsi.jicofo.jibri.PENDING_TIMEOUT" = "90";
+         jicofo.jibri = {
+           brewery-jid = "JibriBrewery@internal.${cfg.hostName}";
+           pending-timeout = "90";
+         };
       })];
     };
 
@@ -451,6 +454,6 @@ in
     };
   };
 
-  meta.doc = ./jitsi-meet.xml;
+  meta.doc = ./jitsi-meet.md;
   meta.maintainers = lib.teams.jitsi.members;
 }
diff --git a/nixos/modules/services/web-apps/jitsi-meet.xml b/nixos/modules/services/web-apps/jitsi-meet.xml
deleted file mode 100644
index ff44c724adf..00000000000
--- a/nixos/modules/services/web-apps/jitsi-meet.xml
+++ /dev/null
@@ -1,55 +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-jitsi-meet">
- <title>Jitsi Meet</title>
- <para>
-   With Jitsi Meet on NixOS you can quickly configure a complete,
-   private, self-hosted video conferencing solution.
- </para>
-
- <section xml:id="module-services-jitsi-basic-usage">
- <title>Basic usage</title>
-   <para>
-     A minimal configuration using Let's Encrypt for TLS certificates looks like this:
-<programlisting>{
-  services.jitsi-meet = {
-    <link linkend="opt-services.jitsi-meet.enable">enable</link> = true;
-    <link linkend="opt-services.jitsi-meet.enable">hostName</link> = "jitsi.example.com";
-  };
-  <link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-  <link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
-  <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
-}</programlisting>
-   </para>
- </section>
-
- <section xml:id="module-services-jitsi-configuration">
- <title>Configuration</title>
-   <para>
-     Here is the minimal configuration with additional configurations:
-<programlisting>{
-  services.jitsi-meet = {
-    <link linkend="opt-services.jitsi-meet.enable">enable</link> = true;
-    <link linkend="opt-services.jitsi-meet.enable">hostName</link> = "jitsi.example.com";
-    <link linkend="opt-services.jitsi-meet.config">config</link> = {
-      enableWelcomePage = false;
-      prejoinPageEnabled = true;
-      defaultLang = "fi";
-    };
-    <link linkend="opt-services.jitsi-meet.interfaceConfig">interfaceConfig</link> = {
-      SHOW_JITSI_WATERMARK = false;
-      SHOW_WATERMARK_FOR_GUESTS = false;
-    };
-  };
-  <link linkend="opt-services.jitsi-videobridge.openFirewall">services.jitsi-videobridge.openFirewall</link> = true;
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-  <link linkend="opt-security.acme.defaults.email">security.acme.email</link> = "me@example.com";
-  <link linkend="opt-security.acme.acceptTerms">security.acme.acceptTerms</link> = true;
-}</programlisting>
-   </para>
- </section>
-
-</chapter>
diff --git a/nixos/modules/services/web-apps/kasmweb/default.nix b/nixos/modules/services/web-apps/kasmweb/default.nix
new file mode 100644
index 00000000000..0d78025ecf0
--- /dev/null
+++ b/nixos/modules/services/web-apps/kasmweb/default.nix
@@ -0,0 +1,275 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.kasmweb;
+in
+{
+  options.services.kasmweb = {
+    enable = lib.mkEnableOption (lib.mdDoc "kasmweb");
+
+    networkSubnet = lib.mkOption {
+      default = "172.20.0.0/16";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        The network subnet to use for the containers.
+      '';
+    };
+
+    postgres = {
+      user = lib.mkOption {
+        default = "kasmweb";
+        type = lib.types.str;
+        description = lib.mdDoc ''
+          Username to use for the postgres database.
+        '';
+      };
+      password = lib.mkOption {
+        default = "kasmweb";
+        type = lib.types.str;
+        description = lib.mdDoc ''
+          password to use for the postgres database.
+        '';
+      };
+    };
+
+    redisPassword = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        password to use for the redis cache.
+      '';
+    };
+
+    defaultAdminPassword = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        default admin password to use.
+      '';
+    };
+
+    defaultUserPassword = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        default user password to use.
+      '';
+    };
+
+    defaultManagerToken = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        default manager token to use.
+      '';
+    };
+
+    defaultGuacToken = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        default guac token to use.
+      '';
+    };
+
+    defaultRegistrationToken = lib.mkOption {
+      default = "kasmweb";
+      type = lib.types.str;
+      description = lib.mdDoc ''
+        default registration token to use.
+      '';
+    };
+
+    datastorePath = lib.mkOption {
+      type = lib.types.str;
+      default = "/var/lib/kasmweb";
+      description = lib.mdDoc ''
+        The directory used to store all data for kasmweb.
+      '';
+    };
+
+    listenAddress = lib.mkOption {
+      type = lib.types.str;
+      default = "0.0.0.0";
+      description = lib.mdDoc ''
+        The address on which kasmweb should listen.
+      '';
+    };
+
+    listenPort = lib.mkOption {
+      type = lib.types.int;
+      default = 443;
+      description = lib.mdDoc ''
+        The port on which kasmweb should listen.
+      '';
+    };
+
+    sslCertificate = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = lib.mdDoc ''
+        The SSL certificate to be used for kasmweb.
+      '';
+    };
+
+    sslCertificateKey = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = lib.mdDoc ''
+        The SSL certificate's key to be used for kasmweb. Make sure to specify
+        this as a string and not a literal path, so that it is not accidentally
+        included in your nixstore.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+
+    systemd.services = {
+      "init-kasmweb" = {
+        wantedBy = [
+          "docker-kasm_db.service"
+        ];
+        before = [
+          "docker-kasm_db.service"
+          "docker-kasm_redis.service"
+          "docker-kasm_db_init.service"
+          "docker-kasm_api.service"
+          "docker-kasm_agent.service"
+          "docker-kasm_manager.service"
+          "docker-kasm_share.service"
+          "docker-kasm_guac.service"
+          "docker-kasm_proxy.service"
+        ];
+        serviceConfig = {
+          Type = "oneshot";
+          ExecStart = pkgs.substituteAll {
+            src = ./initialize_kasmweb.sh;
+            isExecutable = true;
+            binPath = lib.makeBinPath [ pkgs.docker pkgs.openssl pkgs.gnused ];
+            runtimeShell = pkgs.runtimeShell;
+            kasmweb = pkgs.kasmweb;
+            postgresUser = cfg.postgres.user;
+            postgresPassword = cfg.postgres.password;
+            inherit (cfg)
+              datastorePath
+              sslCertificate
+              sslCertificateKey
+              redisPassword
+              defaultUserPassword
+              defaultAdminPassword
+              defaultManagerToken
+              defaultRegistrationToken
+              defaultGuacToken;
+          };
+        };
+      };
+    };
+
+    virtualisation = {
+      oci-containers.containers = {
+        kasm_db = {
+          image = "postgres:12-alpine";
+          environment = {
+            POSTGRES_PASSWORD = cfg.postgres.password;
+            POSTGRES_USER = cfg.postgres.user;
+            POSTGRES_DB = "kasm";
+          };
+          volumes = [
+            "${cfg.datastorePath}/conf/database/data.sql:/docker-entrypoint-initdb.d/data.sql"
+            "${cfg.datastorePath}/conf/database/:/tmp/"
+            "kasmweb_db:/var/lib/postgresql/data"
+          ];
+          extraOptions = [ "--network=kasm_default_network" ];
+        };
+        kasm_db_init = {
+          image = "kasmweb/api:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+            "kasmweb_api_data:/tmp"
+          ];
+          dependsOn = [ "kasm_db" ];
+          entrypoint = "/bin/bash";
+          cmd = [ "/opt/kasm/current/init_seeds.sh" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" ];
+        };
+        kasm_redis = {
+          image = "redis:5-alpine";
+          entrypoint = "/bin/sh";
+          cmd = [
+            "-c"
+            "redis-server --requirepass ${cfg.redisPassword}"
+          ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" ];
+        };
+        kasm_api = {
+          image = "kasmweb/api:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+            "kasmweb_api_data:/tmp"
+          ];
+          dependsOn = [ "kasm_db_init" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host"  ];
+        };
+        kasm_manager = {
+          image = "kasmweb/manager:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+          ];
+          dependsOn = [ "kasm_db" "kasm_api" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" "--read-only"];
+        };
+        kasm_agent = {
+          image = "kasmweb/agent:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+            "/var/run/docker.sock:/var/run/docker.sock"
+            "${pkgs.docker}/bin/docker:/usr/bin/docker"
+            "${cfg.datastorePath}/conf/nginx:/etc/nginx/conf.d"
+          ];
+          dependsOn = [ "kasm_manager" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" "--read-only" ];
+        };
+        kasm_share = {
+          image = "kasmweb/share:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+          ];
+          dependsOn = [ "kasm_db" "kasm_redis" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" "--read-only" ];
+        };
+        kasm_guac = {
+          image = "kasmweb/kasm-guac:${pkgs.kasmweb.version}";
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/:/opt/kasm/current/"
+          ];
+          dependsOn = [ "kasm_db" "kasm_redis" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host" "--read-only" ];
+        };
+        kasm_proxy = {
+          image = "kasmweb/nginx:latest";
+          ports = [ "${cfg.listenAddress}:${toString cfg.listenPort}:443" ];
+          user = "root:root";
+          volumes = [
+            "${cfg.datastorePath}/conf/nginx:/etc/nginx/conf.d:ro"
+            "${cfg.datastorePath}/certs/kasm_nginx.key:/etc/ssl/private/kasm_nginx.key"
+            "${cfg.datastorePath}/certs/kasm_nginx.crt:/etc/ssl/certs/kasm_nginx.crt"
+            "${cfg.datastorePath}/www:/srv/www:ro"
+            "${cfg.datastorePath}/log/nginx:/var/log/external/nginx"
+            "${cfg.datastorePath}/log/logrotate:/var/log/external/logrotate"
+          ];
+          dependsOn = [ "kasm_manager" "kasm_api" "kasm_agent" "kasm_share"
+          "kasm_guac" ];
+          extraOptions = [ "--network=kasm_default_network" "--userns=host"
+          "--network-alias=proxy"];
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/kasmweb/initialize_kasmweb.sh b/nixos/modules/services/web-apps/kasmweb/initialize_kasmweb.sh
new file mode 100644
index 00000000000..dbf043b9869
--- /dev/null
+++ b/nixos/modules/services/web-apps/kasmweb/initialize_kasmweb.sh
@@ -0,0 +1,114 @@
+#! @runtimeShell@
+export PATH=@binPath@:$PATH
+
+mkdir -p @datastorePath@/log
+chmod -R a+rw @datastorePath@
+
+ln -sf @kasmweb@/bin @datastorePath@
+rm -r @datastorePath@/conf
+cp -r @kasmweb@/conf @datastorePath@
+mkdir -p @datastorePath@/conf/nginx/containers.d
+chmod -R a+rw @datastorePath@/conf
+ln -sf @kasmweb@/www @datastorePath@
+
+
+docker network inspect kasm_default_network >/dev/null || docker network create kasm_default_network --subnet @networkSubnet@
+if docker volume inspect kasmweb_db >/dev/null; then
+    source @datastorePath@/ids.env
+    echo 'echo "skipping database init"' > @datastorePath@/init_seeds.sh
+    echo 'while true; do sleep 10 ; done' >> @datastorePath@/init_seeds.sh
+else
+    API_SERVER_ID=$(cat /proc/sys/kernel/random/uuid)
+    MANAGER_ID=$(cat /proc/sys/kernel/random/uuid)
+    SHARE_ID=$(cat /proc/sys/kernel/random/uuid)
+    SERVER_ID=$(cat /proc/sys/kernel/random/uuid)
+    echo "export API_SERVER_ID=$API_SERVER_ID" > @datastorePath@/ids.env
+    echo "export MANAGER_ID=$MANAGER_ID" >> @datastorePath@/ids.env
+    echo "export SHARE_ID=$SHARE_ID" >> @datastorePath@/ids.env
+    echo "export SERVER_ID=$SERVER_ID" >> @datastorePath@/ids.env
+
+    mkdir -p @datastorePath@/certs
+    openssl req -x509 -nodes -days 1825 -newkey rsa:2048 -keyout @datastorePath@/certs/kasm_nginx.key -out @datastorePath@/certs/kasm_nginx.crt -subj "/C=US/ST=VA/L=None/O=None/OU=DoFu/CN=$(hostname)/emailAddress=none@none.none" 2> /dev/null
+
+    docker volume create kasmweb_db
+    rm @datastorePath@/.done_initing_data
+    cat >@datastorePath@/init_seeds.sh <<EOF
+#!/bin/bash
+if [ ! -e /opt/kasm/current/.done_initing_data ]; then
+  sleep 4
+  /usr/bin/kasm_server.so --initialize-database --cfg \
+    /opt/kasm/current/conf/app/api.app.config.yaml \
+    --populate-production \
+    --seed-file \
+    /opt/kasm/current/conf/database/seed_data/default_properties.yaml \
+    2>&1 | grep -v UserWarning
+  /usr/bin/kasm_server.so --cfg \
+    /opt/kasm/current/conf/app/api.app.config.yaml \
+    --populate-production \
+    --seed-file \
+    /opt/kasm/current/conf/database/seed_data/default_agents.yaml \
+    2>&1 | grep -v UserWarning
+  /usr/bin/kasm_server.so --cfg \
+    /opt/kasm/current/conf/app/api.app.config.yaml \
+    --populate-production \
+    --seed-file \
+    /opt/kasm/current/conf/database/seed_data/default_connection_proxies.yaml \
+    2>&1 | grep -v UserWarning
+  /usr/bin/kasm_server.so --cfg \
+    /opt/kasm/current/conf/app/api.app.config.yaml \
+    --populate-production \
+    --seed-file \
+    /opt/kasm/current/conf/database/seed_data/default_images_amd64.yaml \
+    2>&1 | grep -v UserWarning
+  touch /opt/kasm/current/.done_initing_data
+  while true; do sleep 10 ; done
+else
+ echo "skipping database init"
+  while true; do sleep 10 ; done
+fi
+EOF
+fi
+
+chmod +x @datastorePath@/init_seeds.sh
+chmod a+w @datastorePath@/init_seeds.sh
+
+if [ -e @sslCertificate@ ]; then
+    cp @sslCertificate@ @datastorePath@/certs/kasm_nginx.crt
+    cp @sslCertificateKey@ @datastorePath@/certs/kasm_nginx.key
+fi
+
+sed -i -e "s/username.*/username: @postgresUser@/g" \
+    -e "s/password.*/password: @postgresPassword@/g" \
+    -e "s/host.*db/host: kasm_db/g" \
+    -e "s/ssl: true/ssl: false/g" \
+    -e "s/redisPassword.*/redisPassword: @redisPassword@/g" \
+    -e "s/server_hostname.*/server_hostname: kasm_api/g" \
+    -e "s/server_id.*/server_id: $API_SERVER_ID/g" \
+    -e "s/manager_id.*/manager_id: $MANAGER_ID/g" \
+    -e "s/share_id.*/share_id: $SHARE_ID/g" \
+    @datastorePath@/conf/app/api.app.config.yaml
+
+sed -i -e "s/ token:.*/ token: \"@defaultManagerToken@\"/g" \
+    -e "s/hostnames: \['proxy.*/hostnames: \['kasm_proxy'\]/g" \
+    -e "s/server_id.*/server_id: $SERVER_ID/g" \
+    @datastorePath@/conf/app/agent.app.config.yaml
+
+
+sed -i -e "s/password: admin.*/password: \"@defaultAdminPassword@\"/g" \
+    -e "s/password: user.*/password: \"@defaultUserPassword@\"/g" \
+    -e "s/default-manager-token/@defaultManagerToken@/g" \
+    -e "s/default-registration-token/@defaultRegistrationToken@/g" \
+    -e "s/upstream_auth_address:.*/upstream_auth_address: 'proxy'/g" \
+    @datastorePath@/conf/database/seed_data/default_properties.yaml
+
+sed -i -e "s/GUACTOKEN/@defaultGuacToken@/g" \
+    -e "s/APIHOSTNAME/proxy/g" \
+    @datastorePath@/conf/app/kasmguac.app.config.yaml
+
+sed -i -e "s/GUACTOKEN/@defaultGuacToken@/g" \
+    -e "s/APIHOSTNAME/proxy/g" \
+    @datastorePath@/conf/database/seed_data/default_connection_proxies.yaml
+
+sed -i "s/00000000-0000-0000-0000-000000000000/$SERVER_ID/g" \
+    @datastorePath@/conf/database/seed_data/default_agents.yaml
+
diff --git a/nixos/modules/services/web-apps/kavita.nix b/nixos/modules/services/web-apps/kavita.nix
new file mode 100644
index 00000000000..e28b204f1bb
--- /dev/null
+++ b/nixos/modules/services/web-apps/kavita.nix
@@ -0,0 +1,83 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.kavita;
+in {
+  options.services.kavita = {
+    enable = lib.mkEnableOption (lib.mdDoc "Kavita reading server");
+
+    user = lib.mkOption {
+      type = lib.types.str;
+      default = "kavita";
+      description = lib.mdDoc "User account under which Kavita runs.";
+    };
+
+    package = lib.mkPackageOptionMD pkgs "kavita" { };
+
+    dataDir = lib.mkOption {
+      default = "/var/lib/kavita";
+      type = lib.types.str;
+      description = lib.mdDoc "The directory where Kavita stores its state.";
+    };
+
+    tokenKeyFile = lib.mkOption {
+      type = lib.types.path;
+      description = lib.mdDoc ''
+        A file containing the TokenKey, a secret with at 128+ bits.
+        It can be generated with `head -c 32 /dev/urandom | base64`.
+      '';
+    };
+    port = lib.mkOption {
+      default = 5000;
+      type = lib.types.port;
+      description = lib.mdDoc "Port to bind to.";
+    };
+    ipAdresses = lib.mkOption {
+      default = ["0.0.0.0" "::"];
+      type = lib.types.listOf lib.types.str;
+      description = lib.mdDoc "IP Adresses to bind to. The default is to bind
+      to all IPv4 and IPv6 addresses.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.kavita = {
+      description = "Kavita";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      preStart = ''
+        umask u=rwx,g=rx,o=
+        cat > "${cfg.dataDir}/config/appsettings.json" <<EOF
+        {
+          "TokenKey": "$(cat ${cfg.tokenKeyFile})",
+          "Port": ${toString cfg.port},
+          "IpAddresses": "${lib.concatStringsSep "," cfg.ipAdresses}"
+        }
+        EOF
+      '';
+      serviceConfig = {
+        WorkingDirectory = cfg.dataDir;
+        ExecStart = "${lib.getExe cfg.package}";
+        Restart = "always";
+        User = cfg.user;
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDir}'        0750 ${cfg.user} ${cfg.user} - -"
+      "d '${cfg.dataDir}/config' 0750 ${cfg.user} ${cfg.user} - -"
+    ];
+
+    users = {
+      users.${cfg.user} = {
+        description = "kavita service user";
+        isSystemUser = true;
+        group = cfg.user;
+        home = cfg.dataDir;
+      };
+      groups.${cfg.user} = { };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ misterio77 ];
+}
diff --git a/nixos/modules/services/web-apps/keycloak.md b/nixos/modules/services/web-apps/keycloak.md
new file mode 100644
index 00000000000..aa8de40d642
--- /dev/null
+++ b/nixos/modules/services/web-apps/keycloak.md
@@ -0,0 +1,141 @@
+# Keycloak {#module-services-keycloak}
+
+[Keycloak](https://www.keycloak.org/) is an
+open source identity and access management server with support for
+[OpenID Connect](https://openid.net/connect/),
+[OAUTH 2.0](https://oauth.net/2/) and
+[SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0).
+
+## Administration {#module-services-keycloak-admin}
+
+An administrative user with the username
+`admin` is automatically created in the
+`master` realm. Its initial password can be
+configured by setting [](#opt-services.keycloak.initialAdminPassword)
+and defaults to `changeme`. The password is
+not stored safely and should be changed immediately in the
+admin panel.
+
+Refer to the [Keycloak Server Administration Guide](
+  https://www.keycloak.org/docs/latest/server_admin/index.html
+) for information on
+how to administer your Keycloak
+instance.
+
+## Database access {#module-services-keycloak-database}
+
+Keycloak can be used with either PostgreSQL, MariaDB or
+MySQL. Which one is used can be
+configured in [](#opt-services.keycloak.database.type). The selected
+database will automatically be enabled and a database and role
+created unless [](#opt-services.keycloak.database.host) is changed
+from its default of `localhost` or
+[](#opt-services.keycloak.database.createLocally) is set to `false`.
+
+External database access can also be configured by setting
+[](#opt-services.keycloak.database.host),
+[](#opt-services.keycloak.database.name),
+[](#opt-services.keycloak.database.username),
+[](#opt-services.keycloak.database.useSSL) and
+[](#opt-services.keycloak.database.caCert) as
+appropriate. Note that you need to manually create the database
+and allow the configured database user full access to it.
+
+[](#opt-services.keycloak.database.passwordFile)
+must be set to the path to a file containing the password used
+to log in to the database. If [](#opt-services.keycloak.database.host)
+and [](#opt-services.keycloak.database.createLocally)
+are kept at their defaults, the database role
+`keycloak` with that password is provisioned
+on the local database instance.
+
+::: {.warning}
+The path should be provided as a string, not a Nix path, since Nix
+paths are copied into the world readable Nix store.
+:::
+
+## Hostname {#module-services-keycloak-hostname}
+
+The hostname is used to build the public URL used as base for
+all frontend requests and must be configured through
+[](#opt-services.keycloak.settings.hostname).
+
+::: {.note}
+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 [](#opt-services.keycloak.settings.http-relative-path)
+to `/auth`. See the option description
+for more details.
+:::
+
+[](#opt-services.keycloak.settings.hostname-strict-backchannel)
+determines 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.
+
+For more information on hostname configuration, see the [Hostname
+section of the Keycloak Server Installation and Configuration
+Guide](https://www.keycloak.org/server/hostname).
+
+## Setting up TLS/SSL {#module-services-keycloak-tls}
+
+By default, Keycloak won't accept
+unsecured HTTP connections originating from outside its local
+network.
+
+HTTPS support requires a TLS/SSL certificate and a private key,
+both [PEM formatted](https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail).
+Their paths should be set through
+[](#opt-services.keycloak.sslCertificate) and
+[](#opt-services.keycloak.sslCertificateKey).
+
+::: {.warning}
+ The paths should be provided as a strings, not a Nix paths,
+since Nix paths are copied into the world readable Nix store.
+:::
+
+## Themes {#module-services-keycloak-themes}
+
+You can package custom themes and make them visible to
+Keycloak through [](#opt-services.keycloak.themes). See the
+[Themes section of the Keycloak Server Development Guide](
+  https://www.keycloak.org/docs/latest/server_development/#_themes
+) and the description of the aforementioned NixOS option for
+more information.
+
+## Configuration file settings {#module-services-keycloak-settings}
+
+Keycloak server configuration parameters can be set in
+[](#opt-services.keycloak.settings). These correspond
+directly to options in
+{file}`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).
+
+Options containing secret data should be set to an attribute
+set containing the attribute `_secret` - a
+string pointing to a file containing the value the option
+should be set to. See the description of
+[](#opt-services.keycloak.settings) for an example.
+
+## Example configuration {#module-services-keycloak-example-config}
+
+A basic configuration with some custom settings could look like this:
+```
+services.keycloak = {
+  enable = true;
+  settings = {
+    hostname = "keycloak.example.com";
+    hostname-strict-backchannel = true;
+  };
+  initialAdminPassword = "e6Wcm0RrtegMEHl";  # change on first login
+  sslCertificate = "/run/keys/ssl_cert";
+  sslCertificateKey = "/run/keys/ssl_key";
+  database.passwordFile = "/run/keys/db_password";
+};
+```
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
index d52190a2864..a7e4fab8ea2 100644
--- a/nixos/modules/services/web-apps/keycloak.nix
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -674,6 +674,6 @@ in
           mkIf createLocalMySQL (mkDefault dbPkg);
       };
 
-  meta.doc = ./keycloak.xml;
+  meta.doc = ./keycloak.md;
   meta.maintainers = [ maintainers.talyz ];
 }
diff --git a/nixos/modules/services/web-apps/keycloak.xml b/nixos/modules/services/web-apps/keycloak.xml
deleted file mode 100644
index 861756e33ac..00000000000
--- a/nixos/modules/services/web-apps/keycloak.xml
+++ /dev/null
@@ -1,202 +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-keycloak">
- <title>Keycloak</title>
- <para>
-   <link xlink:href="https://www.keycloak.org/">Keycloak</link> is an
-   open source identity and access management server with support for
-   <link xlink:href="https://openid.net/connect/">OpenID
-   Connect</link>, <link xlink:href="https://oauth.net/2/">OAUTH
-   2.0</link> and <link
-   xlink:href="https://en.wikipedia.org/wiki/SAML_2.0">SAML
-   2.0</link>.
- </para>
-   <section xml:id="module-services-keycloak-admin">
-     <title>Administration</title>
-     <para>
-       An administrative user with the username
-       <literal>admin</literal> is automatically created in the
-       <literal>master</literal> realm. Its initial password can be
-       configured by setting <xref linkend="opt-services.keycloak.initialAdminPassword" />
-       and defaults to <literal>changeme</literal>. The password is
-       not stored safely and should be changed immediately in the
-       admin panel.
-     </para>
-
-     <para>
-       Refer to the <link
-       xlink:href="https://www.keycloak.org/docs/latest/server_admin/index.html">
-       Keycloak Server Administration Guide</link> for information on
-       how to administer your <productname>Keycloak</productname>
-       instance.
-     </para>
-   </section>
-
-   <section xml:id="module-services-keycloak-database">
-     <title>Database access</title>
-     <para>
-       <productname>Keycloak</productname> can be used with either
-       <productname>PostgreSQL</productname>,
-       <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>.
-     </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 the database
-       and allow the configured database user full access to it.
-     </para>
-
-     <para>
-       <xref linkend="opt-services.keycloak.database.passwordFile" />
-       must be set to the path to a file containing the password used
-       to log in to the database. If <xref linkend="opt-services.keycloak.database.host" />
-       and <xref linkend="opt-services.keycloak.database.createLocally" />
-       are kept at their defaults, the database role
-       <literal>keycloak</literal> with that password is provisioned
-       on the local database instance.
-     </para>
-
-     <warning>
-       <para>
-         The path should be provided as a string, not a Nix path, since Nix
-         paths are copied into the world readable Nix store.
-       </para>
-     </warning>
-   </section>
-
-   <section xml:id="module-services-keycloak-hostname">
-     <title>Hostname</title>
-     <para>
-       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.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
-       instead use its local hostname or IP address and may also
-       advertise it to clients through its OpenID Connect Discovery
-       endpoint.
-     </para>
-
-     <para>
-        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>
-
-   <section xml:id="module-services-keycloak-tls">
-     <title>Setting up TLS/SSL</title>
-     <para>
-       By default, <productname>Keycloak</productname> won't accept
-       unsecured HTTP connections originating from outside its local
-       network.
-     </para>
-
-     <para>
-       HTTPS support requires a TLS/SSL certificate and a private key,
-       both <link
-       xlink:href="https://en.wikipedia.org/wiki/Privacy-Enhanced_Mail">PEM
-       formatted</link>. Their paths should be set through <xref
-       linkend="opt-services.keycloak.sslCertificate" /> and <xref
-       linkend="opt-services.keycloak.sslCertificateKey" />.
-     </para>
-
-     <warning>
-       <para>
-         The paths should be provided as a strings, not a Nix paths,
-         since Nix paths are copied into the world readable Nix store.
-       </para>
-     </warning>
-   </section>
-
-   <section xml:id="module-services-keycloak-themes">
-     <title>Themes</title>
-     <para>
-        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 the description of the aforementioned NixOS option for
-        more information.
-     </para>
-   </section>
-
-   <section xml:id="module-services-keycloak-settings">
-     <title>Configuration file settings</title>
-     <para>
-       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 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>
-       A basic configuration with some custom settings could look like this:
-<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.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";
-};
-</programlisting>
-     </para>
-
-   </section>
- </chapter>
diff --git a/nixos/modules/services/web-apps/lemmy.nix b/nixos/modules/services/web-apps/lemmy.nix
index 267584dd0ca..f48afd98a6c 100644
--- a/nixos/modules/services/web-apps/lemmy.nix
+++ b/nixos/modules/services/web-apps/lemmy.nix
@@ -6,9 +6,7 @@ let
 in
 {
   meta.maintainers = with maintainers; [ happysalada ];
-  # Don't edit the docbook xml directly, edit the md and generate it:
-  # `pandoc lemmy.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > lemmy.xml`
-  meta.doc = ./lemmy.xml;
+  meta.doc = ./lemmy.md;
 
   imports = [
     (mkRemovedOptionModule [ "services" "lemmy" "jwtSecretPath" ] "As of v0.13.0, Lemmy auto-generates the JWT secret.")
@@ -158,8 +156,8 @@ in
         };
 
         documentation = [
-          "https://join-lemmy.org/docs/en/administration/from_scratch.html"
-          "https://join-lemmy.org/docs"
+          "https://join-lemmy.org/docs/en/admins/from_scratch.html"
+          "https://join-lemmy.org/docs/en/"
         ];
 
         wantedBy = [ "multi-user.target" ];
@@ -187,8 +185,8 @@ in
         };
 
         documentation = [
-          "https://join-lemmy.org/docs/en/administration/from_scratch.html"
-          "https://join-lemmy.org/docs"
+          "https://join-lemmy.org/docs/en/admins/from_scratch.html"
+          "https://join-lemmy.org/docs/en/"
         ];
 
         wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/web-apps/lemmy.xml b/nixos/modules/services/web-apps/lemmy.xml
deleted file mode 100644
index f04316b3c51..00000000000
--- a/nixos/modules/services/web-apps/lemmy.xml
+++ /dev/null
@@ -1,51 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-lemmy">
-  <title>Lemmy</title>
-  <para>
-    Lemmy is a federated alternative to reddit in rust.
-  </para>
-  <section xml:id="module-services-lemmy-quickstart">
-    <title>Quickstart</title>
-    <para>
-      the minimum to start lemmy is
-    </para>
-    <programlisting language="nix">
-services.lemmy = {
-  enable = true;
-  settings = {
-    hostname = &quot;lemmy.union.rocks&quot;;
-    database.createLocally = true;
-  };
-  caddy.enable = true;
-}
-</programlisting>
-    <para>
-      this will start the backend on port 8536 and the frontend on port
-      1234. It will expose your instance with a caddy reverse proxy to
-      the hostname you’ve provided. Postgres will be initialized on that
-      same instance automatically.
-    </para>
-  </section>
-  <section xml:id="module-services-lemmy-usage">
-    <title>Usage</title>
-    <para>
-      On first connection you will be asked to define an admin user.
-    </para>
-  </section>
-  <section xml:id="module-services-lemmy-missing">
-    <title>Missing</title>
-    <itemizedlist spacing="compact">
-      <listitem>
-        <para>
-          Exposing with nginx is not implemented yet.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          This has been tested using a local database with a unix socket
-          connection. Using different database settings will likely
-          require modifications
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/limesurvey.nix b/nixos/modules/services/web-apps/limesurvey.nix
index 7093d1de0da..8e6b39cbdeb 100644
--- a/nixos/modules/services/web-apps/limesurvey.nix
+++ b/nixos/modules/services/web-apps/limesurvey.nix
@@ -32,7 +32,25 @@ in
   # interface
 
   options.services.limesurvey = {
-    enable = mkEnableOption (lib.mdDoc "Limesurvey web application.");
+    enable = mkEnableOption (lib.mdDoc "Limesurvey web application");
+
+    encryptionKey = mkOption {
+      type = types.str;
+      default = "E17687FC77CEE247F0E22BB3ECF27FDE8BEC310A892347EC13013ABA11AA7EB5";
+      description = lib.mdDoc ''
+        This is a 32-byte key used to encrypt variables in the database.
+        You _must_ change this from the default value.
+      '';
+    };
+
+    encryptionNonce = mkOption {
+      type = types.str;
+      default = "1ACC8555619929DB91310BE848025A427B0F364A884FFA77";
+      description = lib.mdDoc ''
+        This is a 24-byte nonce used to encrypt variables in the database.
+        You _must_ change this from the default value.
+      '';
+    };
 
     database = {
       type = mkOption {
@@ -42,6 +60,12 @@ in
         description = lib.mdDoc "Database engine to use.";
       };
 
+      dbEngine = mkOption {
+        type = types.enum [ "MyISAM" "InnoDB" ];
+        default = "InnoDB";
+        description = lib.mdDoc "Database storage engine to use.";
+      };
+
       host = mkOption {
         type = types.str;
         default = "localhost";
@@ -180,6 +204,8 @@ in
       config = {
         tempdir = "${stateDir}/tmp";
         uploaddir = "${stateDir}/upload";
+        encryptionnonce = cfg.encryptionNonce;
+        encryptionsecretboxkey = cfg.encryptionKey;
         force_ssl = mkIf (cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL) "on";
         config.defaultlang = "en";
       };
@@ -200,6 +226,8 @@ in
 
     services.phpfpm.pools.limesurvey = {
       inherit user group;
+      phpPackage = pkgs.php80;
+      phpEnv.DBENGINE = "${cfg.database.dbEngine}";
       phpEnv.LIMESURVEY_CONFIG = "${limesurveyConfig}";
       settings = {
         "listen.owner" = config.services.httpd.user;
@@ -256,11 +284,12 @@ in
       wantedBy = [ "multi-user.target" ];
       before = [ "phpfpm-limesurvey.service" ];
       after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+      environment.DBENGINE = "${cfg.database.dbEngine}";
       environment.LIMESURVEY_CONFIG = limesurveyConfig;
       script = ''
         # update or install the database as required
-        ${pkgs.php}/bin/php ${pkg}/share/limesurvey/application/commands/console.php updatedb || \
-        ${pkgs.php}/bin/php ${pkg}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose
+        ${pkgs.php80}/bin/php ${pkg}/share/limesurvey/application/commands/console.php updatedb || \
+        ${pkgs.php80}/bin/php ${pkg}/share/limesurvey/application/commands/console.php install admin password admin admin@example.com verbose
       '';
       serviceConfig = {
         User = user;
diff --git a/nixos/modules/services/web-apps/mainsail.nix b/nixos/modules/services/web-apps/mainsail.nix
new file mode 100644
index 00000000000..f335d9b015d
--- /dev/null
+++ b/nixos/modules/services/web-apps/mainsail.nix
@@ -0,0 +1,66 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.mainsail;
+  moonraker = config.services.moonraker;
+in
+{
+  options.services.mainsail = {
+    enable = mkEnableOption (lib.mdDoc "a modern and responsive user interface for Klipper");
+
+    package = mkOption {
+      type = types.package;
+      description = lib.mdDoc "Mainsail package to be used in the module";
+      default = pkgs.mainsail;
+      defaultText = literalExpression "pkgs.mainsail";
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      default = "localhost";
+      description = lib.mdDoc "Hostname to serve mainsail on";
+    };
+
+    nginx = mkOption {
+      type = types.submodule
+        (import ../web-servers/nginx/vhost-options.nix { inherit config lib; });
+      default = { };
+      example = literalExpression ''
+        {
+          serverAliases = [ "mainsail.''${config.networking.domain}" ];
+        }
+      '';
+      description = lib.mdDoc "Extra configuration for the nginx virtual host of mainsail.";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.nginx = {
+      enable = true;
+      upstreams.mainsail-apiserver.servers."${moonraker.address}:${toString moonraker.port}" = { };
+      virtualHosts."${cfg.hostName}" = mkMerge [
+        cfg.nginx
+        {
+          root = mkForce "${cfg.package}/share/mainsail";
+          locations = {
+            "/" = {
+              index = "index.html";
+              tryFiles = "$uri $uri/ /index.html";
+            };
+            "/index.html".extraConfig = ''
+              add_header Cache-Control "no-store, no-cache, must-revalidate";
+            '';
+            "/websocket" = {
+              proxyWebsockets = true;
+              proxyPass = "http://mainsail-apiserver/websocket";
+            };
+            "~ ^/(printer|api|access|machine|server)/" = {
+              proxyWebsockets = true;
+              proxyPass = "http://mainsail-apiserver$request_uri";
+            };
+          };
+        }
+      ];
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix
index b6e2309555f..cf5329be489 100644
--- a/nixos/modules/services/web-apps/mastodon.nix
+++ b/nixos/modules/services/web-apps/mastodon.nix
@@ -48,6 +48,8 @@ let
     # User and group
     User = cfg.user;
     Group = cfg.group;
+    # Working directory
+    WorkingDirectory = cfg.package;
     # State directory and mode
     StateDirectory = "mastodon";
     StateDirectoryMode = "0750";
@@ -94,12 +96,14 @@ let
       ] else []
     ) env))));
 
-  mastodonTootctl = pkgs.writeShellScriptBin "mastodon-tootctl" ''
-    #! ${pkgs.runtimeShell}
+  mastodonTootctl = let
+    sourceExtraEnv = lib.concatMapStrings (p: "source ${p}\n") cfg.extraEnvFiles;
+  in pkgs.writeShellScriptBin "mastodon-tootctl" ''
     set -a
     export RAILS_ROOT="${cfg.package}"
     source "${envFile}"
     source /var/lib/mastodon/.secrets_env
+    ${sourceExtraEnv}
 
     sudo=exec
     if [[ "$USER" != ${cfg.user} ]]; then
@@ -108,6 +112,37 @@ let
     $sudo ${cfg.package}/bin/tootctl "$@"
   '';
 
+  sidekiqUnits = lib.attrsets.mapAttrs' (name: processCfg:
+    lib.nameValuePair "mastodon-sidekiq-${name}" (let
+      jobClassArgs = toString (builtins.map (c: "-q ${c}") processCfg.jobClasses);
+      jobClassLabel = toString ([""] ++ processCfg.jobClasses);
+      threads = toString (if processCfg.threads == null then cfg.sidekiqThreads else processCfg.threads);
+    in {
+      after = [ "network.target" "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
+        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
+      requires = [ "mastodon-init-dirs.service" ]
+        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
+        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
+      description = "Mastodon sidekiq${jobClassLabel}";
+      wantedBy = [ "mastodon.target" ];
+      environment = env // {
+        PORT = toString(cfg.sidekiqPort);
+        DB_POOL = threads;
+      };
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/sidekiq ${jobClassArgs} -c ${threads} -r ${cfg.package}";
+        Restart = "always";
+        RestartSec = 20;
+        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
+        WorkingDirectory = cfg.package;
+        # System Call Filtering
+        SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
+      } // cfgService;
+      path = with pkgs; [ file imagemagick ffmpeg ];
+    })
+  ) cfg.sidekiqProcesses;
+
 in {
 
   options = {
@@ -193,12 +228,53 @@ in {
         type = lib.types.port;
         default = 55002;
       };
+
       sidekiqThreads = lib.mkOption {
-        description = lib.mdDoc "Worker threads used by the mastodon-sidekiq service.";
+        description = lib.mdDoc "Worker threads used by the mastodon-sidekiq-all service. If `sidekiqProcesses` is configured and any processes specify null `threads`, this value is used.";
         type = lib.types.int;
         default = 25;
       };
 
+      sidekiqProcesses = lib.mkOption {
+        description = lib.mdDoc "How many Sidekiq processes should be used to handle background jobs, and which job classes they handle. *Read the [upstream documentation](https://docs.joinmastodon.org/admin/scaling/#sidekiq) before configuring this!*";
+        type = with lib.types; attrsOf (submodule {
+          options = {
+            jobClasses = lib.mkOption {
+              type = listOf (enum [ "default" "push" "pull" "mailers" "scheduler" "ingress" ]);
+              description = lib.mdDoc "If not empty, which job classes should be executed by this process. *Only one process should handle the 'scheduler' class. If left empty, this process will handle the 'scheduler' class.*";
+            };
+            threads = lib.mkOption {
+              type = nullOr int;
+              description = lib.mdDoc "Number of threads this process should use for executing jobs. If null, the configured `sidekiqThreads` are used.";
+            };
+          };
+        });
+        default = {
+          all = {
+            jobClasses = [ ];
+            threads = null;
+          };
+        };
+        example = {
+          all = {
+            jobClasses = [ ];
+            threads = null;
+          };
+          ingress = {
+            jobClasses = [ "ingress" ];
+            threads = 5;
+          };
+          default = {
+            jobClasses = [ "default" ];
+            threads = 10;
+          };
+          push-pull = {
+            jobClasses = [ "push" "pull" ];
+            threads = 5;
+          };
+        };
+      };
+
       vapidPublicKeyFile = lib.mkOption {
         description = lib.mdDoc ''
           Path to file containing the public key used for Web Push
@@ -428,6 +504,15 @@ in {
         '';
       };
 
+      extraEnvFiles = lib.mkOption {
+        type = with lib.types; listOf path;
+        default = [];
+        description = lib.mdDoc ''
+          Extra environment files to pass to all mastodon services. Useful for passing down environemntal secrets.
+        '';
+        example = [ "/etc/mastodon/s3config.env" ];
+      };
+
       automaticMigrations = lib.mkOption {
         type = lib.types.bool;
         default = true;
@@ -471,7 +556,7 @@ in {
     };
   };
 
-  config = lib.mkIf cfg.enable {
+  config = lib.mkIf cfg.enable (lib.mkMerge [{
     assertions = [
       {
         assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user);
@@ -502,10 +587,23 @@ in {
             <option>services.mastodon.smtp.authenticate</option> is enabled.
         '';
       }
+      {
+        assertion = 1 == builtins.length
+          (lib.mapAttrsToList
+            (_: v: builtins.elem "scheduler" v.jobClasses || v.jobClasses == [ ])
+            cfg.sidekiqProcesses);
+        message = "There must be one and only one Sidekiq queue in services.mastodon.sidekiqProcesses with jobClass \"scheduler\".";
+      }
     ];
 
     environment.systemPackages = [ mastodonTootctl ];
 
+    systemd.targets.mastodon = {
+      description = "Target for all Mastodon services";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+    };
+
     systemd.services.mastodon-init-dirs = {
       script = ''
         umask 077
@@ -540,7 +638,7 @@ in {
       environment = env;
       serviceConfig = {
         Type = "oneshot";
-        WorkingDirectory = cfg.package;
+        SyslogIdentifier = "mastodon-init-dirs";
         # System Call Filtering
         SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
       } // cfgService;
@@ -580,7 +678,7 @@ in {
       };
       serviceConfig = {
         Type = "oneshot";
-        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ];
+        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
         WorkingDirectory = cfg.package;
         # System Call Filtering
         SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
@@ -598,7 +696,7 @@ in {
       requires = [ "mastodon-init-dirs.service" ]
         ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
         ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
-      wantedBy = [ "multi-user.target" ];
+      wantedBy = [ "mastodon.target" ];
       description = "Mastodon streaming";
       environment = env // (if cfg.enableUnixSocket
         then { SOCKET = "/run/mastodon-streaming/streaming.socket"; }
@@ -608,7 +706,7 @@ in {
         ExecStart = "${cfg.package}/run-streaming.sh";
         Restart = "always";
         RestartSec = 20;
-        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ];
+        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
         WorkingDirectory = cfg.package;
         # Runtime directory and mode
         RuntimeDirectory = "mastodon-streaming";
@@ -625,7 +723,7 @@ in {
       requires = [ "mastodon-init-dirs.service" ]
         ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
         ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
-      wantedBy = [ "multi-user.target" ];
+      wantedBy = [ "mastodon.target" ];
       description = "Mastodon web";
       environment = env // (if cfg.enableUnixSocket
         then { SOCKET = "/run/mastodon-web/web.socket"; }
@@ -635,7 +733,7 @@ in {
         ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
         Restart = "always";
         RestartSec = 20;
-        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ];
+        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
         WorkingDirectory = cfg.package;
         # Runtime directory and mode
         RuntimeDirectory = "mastodon-web";
@@ -646,37 +744,12 @@ in {
       path = with pkgs; [ file imagemagick ffmpeg ];
     };
 
-    systemd.services.mastodon-sidekiq = {
-      after = [ "network.target" "mastodon-init-dirs.service" ]
-        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
-        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
-      requires = [ "mastodon-init-dirs.service" ]
-        ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
-        ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
-      wantedBy = [ "multi-user.target" ];
-      description = "Mastodon sidekiq";
-      environment = env // {
-        PORT = toString(cfg.sidekiqPort);
-        DB_POOL = toString cfg.sidekiqThreads;
-      };
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/sidekiq -c ${toString cfg.sidekiqThreads} -r ${cfg.package}";
-        Restart = "always";
-        RestartSec = 20;
-        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ];
-        WorkingDirectory = cfg.package;
-        # System Call Filtering
-        SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
-      } // cfgService;
-      path = with pkgs; [ file imagemagick ffmpeg ];
-    };
-
     systemd.services.mastodon-media-auto-remove = lib.mkIf cfg.mediaAutoRemove.enable {
       description = "Mastodon media auto remove";
       environment = env;
       serviceConfig = {
         Type = "oneshot";
-        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ];
+        EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
       } // cfgService;
       script = let
         olderThanDays = toString cfg.mediaAutoRemove.olderThanDays;
@@ -746,7 +819,9 @@ in {
     ];
 
     users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
-  };
+  }
+  { systemd.services = sidekiqUnits; }
+  ]);
 
   meta.maintainers = with lib.maintainers; [ happy-river erictapen ];
 
diff --git a/nixos/modules/services/web-apps/matomo-doc.xml b/nixos/modules/services/web-apps/matomo-doc.xml
deleted file mode 100644
index 69d1170e452..00000000000
--- a/nixos/modules/services/web-apps/matomo-doc.xml
+++ /dev/null
@@ -1,107 +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-matomo">
- <title>Matomo</title>
- <para>
-  Matomo is a real-time web analytics application. This module configures
-  php-fpm as backend for Matomo, optionally configuring an nginx vhost as well.
- </para>
- <para>
-  An automatic setup is not suported by Matomo, so you need to configure Matomo
-  itself in the browser-based Matomo setup.
- </para>
- <section xml:id="module-services-matomo-database-setup">
-  <title>Database Setup</title>
-
-  <para>
-   You also need to configure a MariaDB or MySQL database and -user for Matomo
-   yourself, and enter those credentials in your browser. You can use
-   passwordless database authentication via the UNIX_SOCKET authentication
-   plugin with the following SQL commands:
-<programlisting>
-# For MariaDB
-INSTALL PLUGIN unix_socket SONAME 'auth_socket';
-CREATE DATABASE matomo;
-CREATE USER 'matomo'@'localhost' IDENTIFIED WITH unix_socket;
-GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
-
-# For MySQL
-INSTALL PLUGIN auth_socket SONAME 'auth_socket.so';
-CREATE DATABASE matomo;
-CREATE USER 'matomo'@'localhost' IDENTIFIED WITH auth_socket;
-GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
-</programlisting>
-   Then fill in <literal>matomo</literal> as database user and database name,
-   and leave the password field blank. This authentication works by allowing
-   only the <literal>matomo</literal> unix user to authenticate as the
-   <literal>matomo</literal> database user (without needing a password), but no
-   other users. For more information on passwordless login, see
-   <link xlink:href="https://mariadb.com/kb/en/mariadb/unix_socket-authentication-plugin/" />.
-  </para>
-
-  <para>
-   Of course, you can use password based authentication as well, e.g. when the
-   database is not on the same host.
-  </para>
- </section>
- <section xml:id="module-services-matomo-archive-processing">
-  <title>Archive Processing</title>
-
-  <para>
-   This module comes with the systemd service
-   <literal>matomo-archive-processing.service</literal> and a timer that
-   automatically triggers archive processing every hour. This means that you
-   can safely
-   <link xlink:href="https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour">
-   disable browser triggers for Matomo archiving </link> at
-   <literal>Administration > System > General Settings</literal>.
-  </para>
-
-  <para>
-   With automatic archive processing, you can now also enable to
-   <link xlink:href="https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs">
-   delete old visitor logs </link> at <literal>Administration > System >
-   Privacy</literal>, but make sure that you run <literal>systemctl start
-   matomo-archive-processing.service</literal> at least once without errors if
-   you have already collected data before, so that the reports get archived
-   before the source data gets deleted.
-  </para>
- </section>
- <section xml:id="module-services-matomo-backups">
-  <title>Backup</title>
-
-  <para>
-   You only need to take backups of your MySQL database and the
-   <filename>/var/lib/matomo/config/config.ini.php</filename> file. Use a user
-   in the <literal>matomo</literal> group or root to access the file. For more
-   information, see
-   <link xlink:href="https://matomo.org/faq/how-to-install/faq_138/" />.
-  </para>
- </section>
- <section xml:id="module-services-matomo-issues">
-  <title>Issues</title>
-
-  <itemizedlist>
-   <listitem>
-    <para>
-     Matomo will warn you that the JavaScript tracker is not writable. This is
-     because it's located in the read-only nix store. You can safely ignore
-     this, unless you need a plugin that needs JavaScript tracker access.
-    </para>
-   </listitem>
-  </itemizedlist>
- </section>
- <section xml:id="module-services-matomo-other-web-servers">
-  <title>Using other Web Servers than nginx</title>
-
-  <para>
-   You can use other web servers by forwarding calls for
-   <filename>index.php</filename> and <filename>piwik.php</filename> to the
-   <literal><link linkend="opt-services.phpfpm.pools._name_.socket">services.phpfpm.pools.&lt;name&gt;.socket</link></literal> fastcgi unix socket. You can use
-   the nginx configuration in the module code as a reference to what else
-   should be configured.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/matomo.md b/nixos/modules/services/web-apps/matomo.md
new file mode 100644
index 00000000000..f5536a35f7a
--- /dev/null
+++ b/nixos/modules/services/web-apps/matomo.md
@@ -0,0 +1,77 @@
+# Matomo {#module-services-matomo}
+
+Matomo is a real-time web analytics application. This module configures
+php-fpm as backend for Matomo, optionally configuring an nginx vhost as well.
+
+An automatic setup is not suported by Matomo, so you need to configure Matomo
+itself in the browser-based Matomo setup.
+
+## Database Setup {#module-services-matomo-database-setup}
+
+You also need to configure a MariaDB or MySQL database and -user for Matomo
+yourself, and enter those credentials in your browser. You can use
+passwordless database authentication via the UNIX_SOCKET authentication
+plugin with the following SQL commands:
+```
+# For MariaDB
+INSTALL PLUGIN unix_socket SONAME 'auth_socket';
+CREATE DATABASE matomo;
+CREATE USER 'matomo'@'localhost' IDENTIFIED WITH unix_socket;
+GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
+
+# For MySQL
+INSTALL PLUGIN auth_socket SONAME 'auth_socket.so';
+CREATE DATABASE matomo;
+CREATE USER 'matomo'@'localhost' IDENTIFIED WITH auth_socket;
+GRANT ALL PRIVILEGES ON matomo.* TO 'matomo'@'localhost';
+```
+Then fill in `matomo` as database user and database name,
+and leave the password field blank. This authentication works by allowing
+only the `matomo` unix user to authenticate as the
+`matomo` database user (without needing a password), but no
+other users. For more information on passwordless login, see
+<https://mariadb.com/kb/en/mariadb/unix_socket-authentication-plugin/>.
+
+Of course, you can use password based authentication as well, e.g. when the
+database is not on the same host.
+
+## Archive Processing {#module-services-matomo-archive-processing}
+
+This module comes with the systemd service
+`matomo-archive-processing.service` and a timer that
+automatically triggers archive processing every hour. This means that you
+can safely
+[disable browser triggers for Matomo archiving](
+https://matomo.org/docs/setup-auto-archiving/#disable-browser-triggers-for-matomo-archiving-and-limit-matomo-reports-to-updating-every-hour
+) at
+`Administration > System > General Settings`.
+
+With automatic archive processing, you can now also enable to
+[delete old visitor logs](https://matomo.org/docs/privacy/#step-2-delete-old-visitors-logs)
+at `Administration > System > Privacy`, but make sure that you run `systemctl start
+matomo-archive-processing.service` at least once without errors if
+you have already collected data before, so that the reports get archived
+before the source data gets deleted.
+
+## Backup {#module-services-matomo-backups}
+
+You only need to take backups of your MySQL database and the
+{file}`/var/lib/matomo/config/config.ini.php` file. Use a user
+in the `matomo` group or root to access the file. For more
+information, see
+<https://matomo.org/faq/how-to-install/faq_138/>.
+
+## Issues {#module-services-matomo-issues}
+
+  - Matomo will warn you that the JavaScript tracker is not writable. This is
+    because it's located in the read-only nix store. You can safely ignore
+    this, unless you need a plugin that needs JavaScript tracker access.
+
+## Using other Web Servers than nginx {#module-services-matomo-other-web-servers}
+
+You can use other web servers by forwarding calls for
+{file}`index.php` and {file}`piwik.php` to the
+[`services.phpfpm.pools.<name>.socket`](#opt-services.phpfpm.pools._name_.socket)
+fastcgi unix socket. You can use
+the nginx configuration in the module code as a reference to what else
+should be configured.
diff --git a/nixos/modules/services/web-apps/matomo.nix b/nixos/modules/services/web-apps/matomo.nix
index 0435d21ce8a..eadf8b62b97 100644
--- a/nixos/modules/services/web-apps/matomo.nix
+++ b/nixos/modules/services/web-apps/matomo.nix
@@ -325,7 +325,7 @@ in {
   };
 
   meta = {
-    doc = ./matomo-doc.xml;
+    doc = ./matomo.md;
     maintainers = with lib.maintainers; [ florianjacob ];
   };
 }
diff --git a/nixos/modules/services/web-apps/mattermost.nix b/nixos/modules/services/web-apps/mattermost.nix
index 56a53198b3f..db5122e79f0 100644
--- a/nixos/modules/services/web-apps/mattermost.nix
+++ b/nixos/modules/services/web-apps/mattermost.nix
@@ -184,6 +184,22 @@ in
           .tar.gz files.
         '';
       };
+      environmentFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = lib.mdDoc ''
+          Environment file (see {manpage}`systemd.exec(5)`
+          "EnvironmentFile=" section for the syntax) which sets config options
+          for mattermost (see [the mattermost documentation](https://docs.mattermost.com/configure/configuration-settings.html#environment-variables)).
+
+          Settings defined in the environment file will overwrite settings
+          set via nix or via the {option}`services.mattermost.extraConfig`
+          option.
+
+          Useful for setting config options without their value ending up in the
+          (world-readable) nix store, e.g. for a database password.
+        '';
+      };
 
       localDatabaseCreate = mkOption {
         type = types.bool;
@@ -321,6 +337,7 @@ in
           Restart = "always";
           RestartSec = "10";
           LimitNOFILE = "49152";
+          EnvironmentFile = cfg.environmentFile;
         };
         unitConfig.JoinsNamespaceOf = mkIf cfg.localDatabaseCreate "postgresql.service";
       };
diff --git a/nixos/modules/services/web-apps/mediawiki.nix b/nixos/modules/services/web-apps/mediawiki.nix
index 07f29674862..b912d4e87f1 100644
--- a/nixos/modules/services/web-apps/mediawiki.nix
+++ b/nixos/modules/services/web-apps/mediawiki.nix
@@ -8,7 +8,8 @@ let
   cfg = config.services.mediawiki;
   fpm = config.services.phpfpm.pools.mediawiki;
   user = "mediawiki";
-  group = config.services.httpd.group;
+  group = if cfg.webserver == "apache" then "apache" else "mediawiki";
+
   cacheDir = "/var/cache/mediawiki";
   stateDir = "/var/lib/mediawiki";
 
@@ -46,6 +47,15 @@ let
     done
   '';
 
+  dbAddr = if cfg.database.socket == null then
+    "${cfg.database.host}:${toString cfg.database.port}"
+  else if cfg.database.type == "mysql" then
+    "${cfg.database.host}:${cfg.database.socket}"
+  else if cfg.database.type == "postgres" then
+    "${cfg.database.socket}"
+  else
+    throw "Unsupported database type: ${cfg.database.type} for socket: ${cfg.database.socket}";
+
   mediawikiConfig = pkgs.writeText "LocalSettings.php" ''
     <?php
       # Protect against web entry
@@ -64,7 +74,7 @@ let
       $wgScriptPath = "";
 
       ## The protocol and server name to use in fully-qualified URLs
-      $wgServer = "${if cfg.virtualHost.addSSL || cfg.virtualHost.forceSSL || cfg.virtualHost.onlySSL then "https" else "http"}://${cfg.virtualHost.hostName}";
+      $wgServer = "${cfg.url}";
 
       ## The URL path to static resources (images, scripts, etc.)
       $wgResourceBasePath = $wgScriptPath;
@@ -78,8 +88,7 @@ let
       $wgEnableEmail = true;
       $wgEnableUserEmail = true; # UPO
 
-      $wgEmergencyContact = "${if cfg.virtualHost.adminAddr != null then cfg.virtualHost.adminAddr else config.services.httpd.adminAddr}";
-      $wgPasswordSender = $wgEmergencyContact;
+      $wgPasswordSender = "${cfg.passwordSender}";
 
       $wgEnotifUserTalk = false; # UPO
       $wgEnotifWatchlist = false; # UPO
@@ -87,7 +96,8 @@ let
 
       ## Database settings
       $wgDBtype = "${cfg.database.type}";
-      $wgDBserver = "${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}";
+      $wgDBserver = "${dbAddr}";
+      $wgDBport = "${toString cfg.database.port}";
       $wgDBname = "${cfg.database.name}";
       $wgDBuser = "${cfg.database.user}";
       ${optionalString (cfg.database.passwordFile != null) "$wgDBpassword = file_get_contents(\"${cfg.database.passwordFile}\");"}
@@ -180,6 +190,16 @@ in
         description = lib.mdDoc "Which MediaWiki package to use.";
       };
 
+      finalPackage = mkOption {
+        type = types.package;
+        readOnly = true;
+        default = pkg;
+        defaultText = literalExpression "pkg";
+        description = lib.mdDoc ''
+          The final package used by the module. This is the package that will have extensions and skins installed.
+        '';
+      };
+
       name = mkOption {
         type = types.str;
         default = "MediaWiki";
@@ -187,6 +207,22 @@ in
         description = lib.mdDoc "Name of the wiki.";
       };
 
+      url = mkOption {
+        type = types.str;
+        default = if cfg.webserver == "apache" then
+            "${if cfg.httpd.virtualHost.addSSL || cfg.httpd.virtualHost.forceSSL || cfg.httpd.virtualHost.onlySSL then "https" else "http"}://${cfg.httpd.virtualHost.hostName}"
+          else
+            "http://localhost";
+        defaultText = literalExpression ''
+          if cfg.webserver == "apache" then
+            "''${if cfg.httpd.virtualHost.addSSL || cfg.httpd.virtualHost.forceSSL || cfg.httpd.virtualHost.onlySSL then "https" else "http"}://''${cfg.httpd.virtualHost.hostName}"
+          else
+            "http://localhost";
+        '';
+        example = "https://wiki.example.org";
+        description = lib.mdDoc "URL of the wiki.";
+      };
+
       uploadsDir = mkOption {
         type = types.nullOr types.path;
         default = "${stateDir}/uploads";
@@ -202,6 +238,24 @@ in
         example = "/run/keys/mediawiki-password";
       };
 
+      passwordSender = mkOption {
+        type = types.str;
+        default =
+          if cfg.webserver == "apache" then
+            if cfg.httpd.virtualHost.adminAddr != null then
+              cfg.httpd.virtualHost.adminAddr
+            else
+              config.services.httpd.adminAddr else "root@localhost";
+        defaultText = literalExpression ''
+          if cfg.webserver == "apache" then
+            if cfg.httpd.virtualHost.adminAddr != null then
+              cfg.httpd.virtualHost.adminAddr
+            else
+              config.services.httpd.adminAddr else "root@localhost"
+        '';
+        description = lib.mdDoc "Contact address for password reset.";
+      };
+
       skins = mkOption {
         default = {};
         type = types.attrsOf types.path;
@@ -231,6 +285,12 @@ in
         '';
       };
 
+      webserver = mkOption {
+        type = types.enum [ "apache" "none" ];
+        default = "apache";
+        description = lib.mdDoc "Webserver to use.";
+      };
+
       database = {
         type = mkOption {
           type = types.enum [ "mysql" "postgres" "sqlite" "mssql" "oracle" ];
@@ -246,7 +306,8 @@ in
 
         port = mkOption {
           type = types.port;
-          default = 3306;
+          default = if cfg.database.type == "mysql" then 3306 else 5432;
+          defaultText = literalExpression "3306";
           description = lib.mdDoc "Database host port.";
         };
 
@@ -286,14 +347,19 @@ in
 
         socket = mkOption {
           type = types.nullOr types.path;
-          default = if cfg.database.createLocally then "/run/mysqld/mysqld.sock" else null;
+          default = if (cfg.database.type == "mysql" && cfg.database.createLocally) then
+              "/run/mysqld/mysqld.sock"
+            else if (cfg.database.type == "postgres" && cfg.database.createLocally) then
+              "/run/postgresql"
+            else
+              null;
           defaultText = literalExpression "/run/mysqld/mysqld.sock";
           description = lib.mdDoc "Path to the unix socket file to use for authentication.";
         };
 
         createLocally = mkOption {
           type = types.bool;
-          default = cfg.database.type == "mysql";
+          default = cfg.database.type == "mysql" || cfg.database.type == "postgres";
           defaultText = literalExpression "true";
           description = lib.mdDoc ''
             Create the database and database user locally.
@@ -302,7 +368,7 @@ in
         };
       };
 
-      virtualHost = mkOption {
+      httpd.virtualHost = mkOption {
         type = types.submodule (import ../web-servers/apache-httpd/vhost-options.nix);
         example = literalExpression ''
           {
@@ -350,12 +416,16 @@ in
     };
   };
 
+  imports = [
+    (lib.mkRenamedOptionModule [ "services" "mediawiki" "virtualHost" ] [ "services" "mediawiki" "httpd" "virtualHost" ])
+  ];
+
   # implementation
   config = mkIf cfg.enable {
 
     assertions = [
-      { assertion = cfg.database.createLocally -> cfg.database.type == "mysql";
-        message = "services.mediawiki.createLocally is currently only supported for database type 'mysql'";
+      { assertion = cfg.database.createLocally -> (cfg.database.type == "mysql" || cfg.database.type == "postgres");
+        message = "services.mediawiki.createLocally is currently only supported for database type 'mysql' and 'postgres'";
       }
       { assertion = cfg.database.createLocally -> cfg.database.user == user;
         message = "services.mediawiki.database.user must be set to ${user} if services.mediawiki.database.createLocally is set true";
@@ -374,50 +444,64 @@ in
       Vector = "${cfg.package}/share/mediawiki/skins/Vector";
     };
 
-    services.mysql = mkIf cfg.database.createLocally {
+    services.mysql = mkIf (cfg.database.type == "mysql" && cfg.database.createLocally) {
       enable = true;
       package = mkDefault pkgs.mariadb;
       ensureDatabases = [ cfg.database.name ];
-      ensureUsers = [
-        { name = cfg.database.user;
-          ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
-        }
-      ];
+      ensureUsers = [{
+        name = cfg.database.user;
+        ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
+      }];
+    };
+
+    services.postgresql = mkIf (cfg.database.type == "postgres" && cfg.database.createLocally) {
+      enable = true;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [{
+        name = cfg.database.user;
+        ensurePermissions = { "DATABASE \"${cfg.database.name}\"" = "ALL PRIVILEGES"; };
+      }];
     };
 
     services.phpfpm.pools.mediawiki = {
       inherit user group;
       phpEnv.MEDIAWIKI_CONFIG = "${mediawikiConfig}";
-      settings = {
+      settings = (if (cfg.webserver == "apache") then {
         "listen.owner" = config.services.httpd.user;
         "listen.group" = config.services.httpd.group;
-      } // cfg.poolConfig;
+      } else {
+        "listen.owner" = user;
+        "listen.group" = group;
+      }) // cfg.poolConfig;
     };
 
-    services.httpd = {
+    services.httpd = lib.mkIf (cfg.webserver == "apache") {
       enable = true;
       extraModules = [ "proxy_fcgi" ];
-      virtualHosts.${cfg.virtualHost.hostName} = mkMerge [ cfg.virtualHost {
-        documentRoot = mkForce "${pkg}/share/mediawiki";
-        extraConfig = ''
-          <Directory "${pkg}/share/mediawiki">
-            <FilesMatch "\.php$">
-              <If "-f %{REQUEST_FILENAME}">
-                SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
-              </If>
-            </FilesMatch>
-
-            Require all granted
-            DirectoryIndex index.php
-            AllowOverride All
-          </Directory>
-        '' + optionalString (cfg.uploadsDir != null) ''
-          Alias "/images" "${cfg.uploadsDir}"
-          <Directory "${cfg.uploadsDir}">
-            Require all granted
-          </Directory>
-        '';
-      } ];
+      virtualHosts.${cfg.httpd.virtualHost.hostName} = mkMerge [
+        cfg.httpd.virtualHost
+        {
+          documentRoot = mkForce "${pkg}/share/mediawiki";
+          extraConfig = ''
+            <Directory "${pkg}/share/mediawiki">
+              <FilesMatch "\.php$">
+                <If "-f %{REQUEST_FILENAME}">
+                  SetHandler "proxy:unix:${fpm.socket}|fcgi://localhost/"
+                </If>
+              </FilesMatch>
+
+              Require all granted
+              DirectoryIndex index.php
+              AllowOverride All
+            </Directory>
+          '' + optionalString (cfg.uploadsDir != null) ''
+            Alias "/images" "${cfg.uploadsDir}"
+            <Directory "${cfg.uploadsDir}">
+              Require all granted
+            </Directory>
+          '';
+        }
+      ];
     };
 
     systemd.tmpfiles.rules = [
@@ -431,7 +515,8 @@ in
     systemd.services.mediawiki-init = {
       wantedBy = [ "multi-user.target" ];
       before = [ "phpfpm-mediawiki.service" ];
-      after = optional cfg.database.createLocally "mysql.service";
+      after = optional (cfg.database.type == "mysql" && cfg.database.createLocally) "mysql.service"
+              ++ optional (cfg.database.type == "postgres" && cfg.database.createLocally) "postgresql.service";
       script = ''
         if ! test -e "${stateDir}/secret.key"; then
           tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c 64 > ${stateDir}/secret.key
@@ -442,7 +527,7 @@ in
         ${pkgs.php}/bin/php ${pkg}/share/mediawiki/maintenance/install.php \
           --confpath /tmp \
           --scriptpath / \
-          --dbserver ${cfg.database.host}${optionalString (cfg.database.socket != null) ":${cfg.database.socket}"} \
+          --dbserver "${dbAddr}" \
           --dbport ${toString cfg.database.port} \
           --dbname ${cfg.database.name} \
           ${optionalString (cfg.database.tablePrefix != null) "--dbprefix ${cfg.database.tablePrefix}"} \
@@ -464,12 +549,14 @@ in
       };
     };
 
-    systemd.services.httpd.after = optional (cfg.database.createLocally && cfg.database.type == "mysql") "mysql.service";
+    systemd.services.httpd.after = optional (cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "mysql") "mysql.service"
+      ++ optional (cfg.webserver == "apache" && cfg.database.createLocally && cfg.database.type == "postgres") "postgresql.service";
 
     users.users.${user} = {
       group = group;
       isSystemUser = true;
     };
+    users.groups.${group} = {};
 
     environment.systemPackages = [ mediawikiScripts ];
   };
diff --git a/nixos/modules/services/web-apps/monica.nix b/nixos/modules/services/web-apps/monica.nix
new file mode 100644
index 00000000000..442044fedb1
--- /dev/null
+++ b/nixos/modules/services/web-apps/monica.nix
@@ -0,0 +1,468 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}:
+with lib; let
+  cfg = config.services.monica;
+  monica = pkgs.monica.override {
+    dataDir = cfg.dataDir;
+  };
+  db = cfg.database;
+  mail = cfg.mail;
+
+  user = cfg.user;
+  group = cfg.group;
+
+  # shell script for local administration
+  artisan = pkgs.writeScriptBin "monica" ''
+    #! ${pkgs.runtimeShell}
+    cd ${monica}
+    sudo() {
+      if [[ "$USER" != ${user} ]]; then
+        exec /run/wrappers/bin/sudo -u ${user} "$@"
+      else
+        exec "$@"
+      fi
+    }
+    sudo ${pkgs.php}/bin/php artisan "$@"
+  '';
+
+  tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
+in {
+  options.services.monica = {
+    enable = mkEnableOption (lib.mdDoc "monica");
+
+    user = mkOption {
+      default = "monica";
+      description = lib.mdDoc "User monica runs as.";
+      type = types.str;
+    };
+
+    group = mkOption {
+      default = "monica";
+      description = lib.mdDoc "Group monica runs as.";
+      type = types.str;
+    };
+
+    appKeyFile = mkOption {
+      description = lib.mdDoc ''
+        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/monica-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 = "monica.example.com";
+      description = lib.mdDoc ''
+        The hostname to serve monica on.
+      '';
+    };
+
+    appURL = mkOption {
+      description = lib.mdDoc ''
+        The root URL that you want to host monica on. All URLs in monica 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>php artisan monica: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 = lib.mdDoc "monica data directory";
+      default = "/var/lib/monica";
+      type = types.path;
+    };
+
+    database = {
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = lib.mdDoc "Database host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 3306;
+        description = lib.mdDoc "Database host port.";
+      };
+      name = mkOption {
+        type = types.str;
+        default = "monica";
+        description = lib.mdDoc "Database name.";
+      };
+      user = mkOption {
+        type = types.str;
+        default = user;
+        defaultText = lib.literalExpression "user";
+        description = lib.mdDoc "Database username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/monica-dbpassword";
+        description = lib.mdDoc ''
+          A file containing the password corresponding to
+          <option>database.user</option>.
+        '';
+      };
+      createLocally = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc "Create the database and database user locally.";
+      };
+    };
+
+    mail = {
+      driver = mkOption {
+        type = types.enum ["smtp" "sendmail"];
+        default = "smtp";
+        description = lib.mdDoc "Mail driver to use.";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = lib.mdDoc "Mail host address.";
+      };
+      port = mkOption {
+        type = types.port;
+        default = 1025;
+        description = lib.mdDoc "Mail host port.";
+      };
+      fromName = mkOption {
+        type = types.str;
+        default = "monica";
+        description = lib.mdDoc "Mail \"from\" name.";
+      };
+      from = mkOption {
+        type = types.str;
+        default = "mail@monica.com";
+        description = lib.mdDoc "Mail \"from\" email.";
+      };
+      user = mkOption {
+        type = with types; nullOr str;
+        default = null;
+        example = "monica";
+        description = lib.mdDoc "Mail username.";
+      };
+      passwordFile = mkOption {
+        type = with types; nullOr path;
+        default = null;
+        example = "/run/keys/monica-mailpassword";
+        description = lib.mdDoc ''
+          A file containing the password corresponding to
+          <option>mail.user</option>.
+        '';
+      };
+      encryption = mkOption {
+        type = with types; nullOr (enum ["tls"]);
+        default = null;
+        description = lib.mdDoc "SMTP encryption mechanism to use.";
+      };
+    };
+
+    maxUploadSize = mkOption {
+      type = types.str;
+      default = "18M";
+      example = "1G";
+      description = lib.mdDoc "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 = lib.mdDoc ''
+        Options for the monica 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 = ''
+        {
+          serverAliases = [
+            "monica.''${config.networking.domain}"
+          ];
+          # To enable encryption and let let's encrypt take care of certificate
+          forceSSL = true;
+          enableACME = true;
+        }
+      '';
+      description = lib.mdDoc ''
+        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 str;
+                  description = lib.mdDoc ''
+                    The path to a file containing the value the
+                    option should be set to in the final
+                    configuration file.
+                  '';
+                };
+              };
+            })));
+      default = {};
+      example = ''
+        {
+          ALLOWED_IFRAME_HOSTS = "https://example.com";
+          WKHTMLTOPDF = "/home/user/bins/wkhtmltopdf";
+          AUTH_METHOD = "oidc";
+          OIDC_NAME = "MyLogin";
+          OIDC_DISPLAY_NAME_CLAIMS = "name";
+          OIDC_CLIENT_ID = "monica";
+          OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
+          OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
+          OIDC_ISSUER_DISCOVER = true;
+        }
+      '';
+      description = lib.mdDoc ''
+        monica configuration options to set in the
+        <filename>.env</filename> file.
+
+        Refer to <link xlink:href="https://github.com/monicahq/monica"/>
+        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.monica.database.user must be set to ${user} if services.monica.database.createLocally is set true.";
+      }
+      {
+        assertion = db.createLocally -> db.passwordFile == null;
+        message = "services.monica.database.passwordFile cannot be specified if services.monica.database.createLocally is set to true.";
+      }
+    ];
+
+    services.monica.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;
+      MAIL_DRIVER = mail.driver;
+      MAIL_FROM_NAME = mail.fromName;
+      MAIL_FROM = mail.from;
+      MAIL_HOST = mail.host;
+      MAIL_PORT = mail.port;
+      MAIL_USERNAME = mail.user;
+      MAIL_ENCRYPTION = mail.encryption;
+      DB_PASSWORD._secret = db.passwordFile;
+      MAIL_PASSWORD._secret = mail.passwordFile;
+      APP_SERVICES_CACHE = "/run/monica/cache/services.php";
+      APP_PACKAGES_CACHE = "/run/monica/cache/packages.php";
+      APP_CONFIG_CACHE = "/run/monica/cache/config.php";
+      APP_ROUTES_CACHE = "/run/monica/cache/routes-v7.php";
+      APP_EVENTS_CACHE = "/run/monica/cache/events.php";
+      SESSION_SECURE_COOKIE = tlsEnabled;
+    };
+
+    environment.systemPackages = [artisan];
+
+    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.monica = {
+      inherit user group;
+      phpOptions = ''
+        log_errors = on
+        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;
+      recommendedTlsSettings = true;
+      recommendedOptimisation = true;
+      recommendedGzipSettings = true;
+      recommendedBrotliSettings = true;
+      recommendedProxySettings = true;
+      virtualHosts.${cfg.hostname} = mkMerge [
+        cfg.nginx
+        {
+          root = mkForce "${monica}/public";
+          locations = {
+            "/" = {
+              index = "index.php";
+              tryFiles = "$uri $uri/ /index.php?$query_string";
+            };
+            "~ \.php$".extraConfig = ''
+              fastcgi_pass unix:${config.services.phpfpm.pools."monica".socket};
+            '';
+            "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
+              extraConfig = "expires 365d;";
+            };
+          };
+        }
+      ];
+    };
+
+    systemd.services.monica-setup = {
+      description = "Preperation tasks for monica";
+      before = ["phpfpm-monica.service"];
+      after = optional db.createLocally "mysql.service";
+      wantedBy = ["multi-user.target"];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        User = user;
+        UMask = 077;
+        WorkingDirectory = "${monica}";
+        RuntimeDirectory = "monica/cache";
+        RuntimeDirectoryMode = 0700;
+      };
+      path = [pkgs.replace-secret];
+      script = let
+        isSecret = v: isAttrs v && v ? _secret && isString v._secret;
+        monicaEnvVars = 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}";
+          };
+        };
+        secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
+        mkSecretReplacement = file: ''
+          replace-secret ${escapeShellArgs [(builtins.hashString "sha256" file) file "${cfg.dataDir}/.env"]}
+        '';
+        secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
+        filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{} null])) cfg.config;
+        monicaEnv = pkgs.writeText "monica.env" (monicaEnvVars filteredConfig);
+      in ''
+        # error handling
+        set -euo pipefail
+
+        # create .env file
+        install -T -m 0600 -o ${user} ${monicaEnv} "${cfg.dataDir}/.env"
+        ${secretReplacements}
+        if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
+          sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
+        fi
+
+        # migrate & seed db
+        ${pkgs.php}/bin/php artisan key:generate --force
+        ${pkgs.php}/bin/php artisan setup:production -v --force
+      '';
+    };
+
+    systemd.services.monica-scheduler = {
+      description = "Background tasks for monica";
+      startAt = "minutely";
+      after = ["monica-setup.service"];
+      serviceConfig = {
+        Type = "oneshot";
+        User = user;
+        WorkingDirectory = "${monica}";
+        ExecStart = "${pkgs.php}/bin/php ${monica}/artisan schedule:run -v";
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d ${cfg.dataDir}                            0710 ${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 == "monica") {
+        monica = {
+          inherit group;
+          isSystemUser = true;
+        };
+        "${config.services.nginx.user}".extraGroups = [group];
+      };
+      groups = mkIf (group == "monica") {
+        monica = {};
+      };
+    };
+  };
+}
+
diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix
index 5f8d9c5b15f..b617e9a5937 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.php80.buildEnv {
+  phpExt = pkgs.php81.buildEnv {
     extensions = { all, ... }: with all; [ iconv mbstring curl openssl tokenizer soap ctype zip gd simplexml dom intl sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter opcache exif sodium ];
     extraConfig = "max_input_vars = 5000";
   };
diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix
index e028f16004e..0ecb20e8c2c 100644
--- a/nixos/modules/services/web-apps/netbox.nix
+++ b/nixos/modules/services/web-apps/netbox.nix
@@ -4,45 +4,17 @@ with lib;
 
 let
   cfg = config.services.netbox;
+  pythonFmt = pkgs.formats.pythonVars {};
   staticDir = cfg.dataDir + "/static";
-  configFile = pkgs.writeTextFile {
-    name = "configuration.py";
-    text = ''
-      STATIC_ROOT = '${staticDir}'
-      MEDIA_ROOT = '${cfg.dataDir}/media'
-      REPORTS_ROOT = '${cfg.dataDir}/reports'
-      SCRIPTS_ROOT = '${cfg.dataDir}/scripts'
-
-      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}
-    '';
+
+  settingsFile = pythonFmt.generate "netbox-settings.py" cfg.settings;
+  extraConfigFile = pkgs.writeTextFile {
+    name = "netbox-extraConfig.py";
+    text = cfg.extraConfig;
   };
-  pkg = (pkgs.netbox.overrideAttrs (old: {
+  configFile = pkgs.concatText "configuration.py" [ settingsFile extraConfigFile ];
+
+  pkg = (cfg.package.overrideAttrs (old: {
     installPhase = old.installPhase + ''
       ln -s ${configFile} $out/opt/netbox/netbox/netbox/configuration.py
     '' + optionalString cfg.enableLdap ''
@@ -70,6 +42,30 @@ in {
       '';
     };
 
+    settings = lib.mkOption {
+      description = lib.mdDoc ''
+        Configuration options to set in `configuration.py`.
+        See the [documentation](https://docs.netbox.dev/en/stable/configuration/) for more possible options.
+      '';
+
+      default = { };
+
+      type = lib.types.submodule {
+        freeformType = pythonFmt.type;
+
+        options = {
+          ALLOWED_HOSTS = lib.mkOption {
+            type = with lib.types; listOf str;
+            default = ["*"];
+            description = lib.mdDoc ''
+              A list of valid fully-qualified domain names (FQDNs) and/or IP
+              addresses that can be used to reach the NetBox service.
+            '';
+          };
+        };
+      };
+    };
+
     listenAddress = mkOption {
       type = types.str;
       default = "[::1]";
@@ -78,6 +74,17 @@ in {
       '';
     };
 
+    package = mkOption {
+      type = types.package;
+      default = if versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox else pkgs.netbox_3_3;
+      defaultText = literalExpression ''
+        if versionAtLeast config.system.stateVersion "23.05" then pkgs.netbox else pkgs.netbox_3_3;
+      '';
+      description = lib.mdDoc ''
+        NetBox package to use.
+      '';
+    };
+
     port = mkOption {
       type = types.port;
       default = 8001;
@@ -117,7 +124,7 @@ in {
       default = "";
       description = lib.mdDoc ''
         Additional lines of configuration appended to the `configuration.py`.
-        See the [documentation](https://netbox.readthedocs.io/en/stable/configuration/optional-settings/) for more possible options.
+        See the [documentation](https://docs.netbox.dev/en/stable/configuration/) for more possible options.
       '';
     };
 
@@ -138,11 +145,90 @@ in {
         Path to the Configuration-File for LDAP-Authentication, will be loaded as `ldap_config.py`.
         See the [documentation](https://netbox.readthedocs.io/en/stable/installation/6-ldap/#configuration) for possible options.
       '';
+      example = ''
+        import ldap
+        from django_auth_ldap.config import LDAPSearch, PosixGroupType
+
+        AUTH_LDAP_SERVER_URI = "ldaps://ldap.example.com/"
+
+        AUTH_LDAP_USER_SEARCH = LDAPSearch(
+            "ou=accounts,ou=posix,dc=example,dc=com",
+            ldap.SCOPE_SUBTREE,
+            "(uid=%(user)s)",
+        )
+
+        AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
+            "ou=groups,ou=posix,dc=example,dc=com",
+            ldap.SCOPE_SUBTREE,
+            "(objectClass=posixGroup)",
+        )
+        AUTH_LDAP_GROUP_TYPE = PosixGroupType()
+
+        # Mirror LDAP group assignments.
+        AUTH_LDAP_MIRROR_GROUPS = True
+
+        # For more granular permissions, we can map LDAP groups to Django groups.
+        AUTH_LDAP_FIND_GROUP_PERMS = True
+      '';
     };
   };
 
   config = mkIf cfg.enable {
-    services.netbox.plugins = mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]);
+    services.netbox = {
+      plugins = mkIf cfg.enableLdap (ps: [ ps.django-auth-ldap ]);
+      settings = {
+        STATIC_ROOT = staticDir;
+        MEDIA_ROOT = "${cfg.dataDir}/media";
+        REPORTS_ROOT = "${cfg.dataDir}/reports";
+        SCRIPTS_ROOT = "${cfg.dataDir}/scripts";
+
+        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;
+            };
+        };
+
+        REMOTE_AUTH_BACKEND = lib.mkIf cfg.enableLdap "netbox.authentication.LDAPBackend";
+
+        LOGGING = lib.mkDefault {
+          version = 1;
+
+          formatters.precise.format = "[%(levelname)s@%(name)s] %(message)s";
+
+          handlers.console = {
+            class = "logging.StreamHandler";
+            formatter = "precise";
+          };
+
+          # log to console/systemd instead of file
+          root = {
+            level = "INFO";
+            handlers = [ "console" ];
+          };
+        };
+      };
+
+      extraConfig = ''
+        with open("${cfg.secretKeyFile}", "r") as file:
+            SECRET_KEY = file.readline()
+      '';
+    };
 
     services.redis.servers.netbox.enable = true;
 
diff --git a/nixos/modules/services/web-apps/nextcloud-notify_push.nix b/nixos/modules/services/web-apps/nextcloud-notify_push.nix
new file mode 100644
index 00000000000..52a772f1214
--- /dev/null
+++ b/nixos/modules/services/web-apps/nextcloud-notify_push.nix
@@ -0,0 +1,96 @@
+{ config, options, lib, pkgs, ... }:
+
+let
+  cfg = config.services.nextcloud.notify_push;
+in
+{
+  options.services.nextcloud.notify_push = {
+    enable = lib.mkEnableOption (lib.mdDoc "Notify push");
+
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = pkgs.nextcloud-notify_push;
+      defaultText = lib.literalMD "pkgs.nextcloud-notify_push";
+      description = lib.mdDoc "Which package to use for notify_push";
+    };
+
+    socketPath = lib.mkOption {
+      type = lib.types.str;
+      default = "/run/nextcloud-notify_push/sock";
+      description = lib.mdDoc "Socket path to use for notify_push";
+    };
+
+    logLevel = lib.mkOption {
+      type = lib.types.enum [ "error" "warn" "info" "debug" "trace" ];
+      default = "error";
+      description = lib.mdDoc "Log level";
+    };
+  } // (
+    lib.genAttrs [
+      "dbtype"
+      "dbname"
+      "dbuser"
+      "dbpassFile"
+      "dbhost"
+      "dbport"
+      "dbtableprefix"
+    ] (
+      opt: options.services.nextcloud.config.${opt} // {
+        default = config.services.nextcloud.config.${opt};
+        defaultText = "config.services.nextcloud.config.${opt}";
+      }
+    )
+  );
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.nextcloud-notify_push = let
+      nextcloudUrl = "http${lib.optionalString config.services.nextcloud.https "s"}://${config.services.nextcloud.hostName}";
+    in {
+      description = "Push daemon for Nextcloud clients";
+      documentation = [ "https://github.com/nextcloud/notify_push" ];
+      after = [ "phpfpm-nextcloud.service" ];
+      wantedBy = [ "multi-user.target" ];
+      environment = {
+        NEXTCLOUD_URL = nextcloudUrl;
+        SOCKET_PATH = cfg.socketPath;
+        DATABASE_PREFIX = cfg.dbtableprefix;
+        LOG = cfg.logLevel;
+      };
+      postStart = ''
+        ${config.services.nextcloud.occ}/bin/nextcloud-occ notify_push:setup ${nextcloudUrl}/push
+      '';
+      script = let
+        dbType = if cfg.dbtype == "pgsql" then "postgresql" else cfg.dbtype;
+        dbUser = lib.optionalString (cfg.dbuser != null) cfg.dbuser;
+        dbPass = lib.optionalString (cfg.dbpassFile != null) ":$DATABASE_PASSWORD";
+        isSocket = lib.hasPrefix "/" (toString cfg.dbhost);
+        dbHost = lib.optionalString (cfg.dbhost != null) (if
+          isSocket then
+            if dbType == "postgresql" then "?host=${cfg.dbhost}" else
+            if dbType == "mysql" then "?socket=${cfg.dbhost}" else throw "unsupported dbtype"
+          else
+            "@${cfg.dbhost}");
+        dbName = lib.optionalString (cfg.dbname != null) "/${cfg.dbname}";
+        dbUrl = "${dbType}://${dbUser}${dbPass}${lib.optionalString (!isSocket) dbHost}${dbName}${lib.optionalString isSocket dbHost}";
+      in lib.optionalString (dbPass != "") ''
+        export DATABASE_PASSWORD="$(<"${cfg.dbpassFile}")"
+      '' + ''
+        export DATABASE_URL="${dbUrl}"
+        ${cfg.package}/bin/notify_push '${config.services.nextcloud.datadir}/config/config.php'
+      '';
+      serviceConfig = {
+        User = "nextcloud";
+        Group = "nextcloud";
+        RuntimeDirectory = [ "nextcloud-notify_push" ];
+        Restart = "on-failure";
+        RestartSec = "5s";
+      };
+    };
+
+    services.nginx.virtualHosts.${config.services.nextcloud.hostName}.locations."^~ /push/" = {
+      proxyPass = "http://unix:${cfg.socketPath}";
+      proxyWebsockets = true;
+      recommendedProxySettings = true;
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/nextcloud.md b/nixos/modules/services/web-apps/nextcloud.md
new file mode 100644
index 00000000000..6ecfc6ca7e4
--- /dev/null
+++ b/nixos/modules/services/web-apps/nextcloud.md
@@ -0,0 +1,225 @@
+# Nextcloud {#module-services-nextcloud}
+
+[Nextcloud](https://nextcloud.com/) is an open-source,
+self-hostable cloud platform. The server setup can be automated using
+[services.nextcloud](#opt-services.nextcloud.enable). A
+desktop client is packaged at `pkgs.nextcloud-client`.
+
+The current default by NixOS is `nextcloud26` which is also the latest
+major version available.
+
+## Basic usage {#module-services-nextcloud-basic-usage}
+
+Nextcloud is a PHP-based application which requires an HTTP server
+([`services.nextcloud`](#opt-services.nextcloud.enable)
+and optionally supports
+[`services.nginx`](#opt-services.nginx.enable)).
+
+For the database, you can set
+[`services.nextcloud.config.dbtype`](#opt-services.nextcloud.config.dbtype) to
+either `sqlite` (the default), `mysql`, or `pgsql`. For the last two, by
+default, a local database will be created and nextcloud will connect to it via
+socket; this can be disabled by setting
+[`services.nextcloud.database.createLocally`](#opt-services.nextcloud.database.createLocally)
+to `false`.
+
+A very basic configuration may look like this:
+```
+{ pkgs, ... }:
+{
+  services.nextcloud = {
+    enable = true;
+    hostName = "nextcloud.tld";
+    config = {
+      dbtype = "pgsql";
+      adminpassFile = "/path/to/admin-pass-file";
+    };
+  };
+
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+}
+```
+
+The `hostName` option is used internally to configure an HTTP
+server using [`PHP-FPM`](https://php-fpm.org/)
+and `nginx`. The `config` attribute set is
+used by the imperative installer and all values are written to an additional file
+to ensure that changes can be applied by changing the module's options.
+
+In case the application serves multiple domains (those are checked with
+[`$_SERVER['HTTP_HOST']`](http://php.net/manual/en/reserved.variables.server.php))
+it's needed to add them to
+[`services.nextcloud.config.extraTrustedDomains`](#opt-services.nextcloud.config.extraTrustedDomains).
+
+Auto updates for Nextcloud apps can be enabled using
+[`services.nextcloud.autoUpdateApps`](#opt-services.nextcloud.autoUpdateApps.enable).
+
+## Common problems {#module-services-nextcloud-pitfalls-during-upgrade}
+
+  - **General notes.**
+    Unfortunately Nextcloud appears to be very stateful when it comes to
+    managing its own configuration. The config file lives in the home directory
+    of the `nextcloud` user (by default
+    `/var/lib/nextcloud/config/config.php`) and is also used to
+    track several states of the application (e.g., whether installed or not).
+
+     All configuration parameters are also stored in
+    {file}`/var/lib/nextcloud/config/override.config.php` which is generated by
+    the module and linked from the store to ensure that all values from
+    {file}`config.php` can be modified by the module.
+    However {file}`config.php` manages the application's state and shouldn't be
+    touched manually because of that.
+
+    ::: {.warning}
+    Don't delete {file}`config.php`! This file
+    tracks the application's state and a deletion can cause unwanted
+    side-effects!
+    :::
+
+    ::: {.warning}
+    Don't rerun `nextcloud-occ maintenance:install`!
+    This command tries to install the application
+    and can cause unwanted side-effects!
+    :::
+  - **Multiple version upgrades.**
+    Nextcloud doesn't allow to move more than one major-version forward. E.g., if you're on
+    `v16`, you cannot upgrade to `v18`, you need to upgrade to
+    `v17` first. This is ensured automatically as long as the
+    [stateVersion](#opt-system.stateVersion) is declared properly. In that case
+    the oldest version available (one major behind the one from the previous NixOS
+    release) will be selected by default and the module will generate a warning that reminds
+    the user to upgrade to latest Nextcloud *after* that deploy.
+  - **`Error: Command "upgrade" is not defined.`**
+    This error usually occurs if the initial installation
+    ({command}`nextcloud-occ maintenance:install`) has failed. After that, the application
+    is not installed, but the upgrade is attempted to be executed. Further context can
+    be found in [NixOS/nixpkgs#111175](https://github.com/NixOS/nixpkgs/issues/111175).
+
+    First of all, it makes sense to find out what went wrong by looking at the logs
+    of the installation via {command}`journalctl -u nextcloud-setup` and try to fix
+    the underlying issue.
+
+    - If this occurs on an *existing* setup, this is most likely because
+      the maintenance mode is active. It can be deactivated by running
+      {command}`nextcloud-occ maintenance:mode --off`. It's advisable though to
+      check the logs first on why the maintenance mode was activated.
+    - ::: {.warning}
+      Only perform the following measures on
+      *freshly installed instances!*
+      :::
+
+      A re-run of the installer can be forced by *deleting*
+      {file}`/var/lib/nextcloud/config/config.php`. This is the only time
+      advisable because the fresh install doesn't have any state that can be lost.
+      In case that doesn't help, an entire re-creation can be forced via
+      {command}`rm -rf ~nextcloud/`.
+
+  - **Server-side encryption.**
+    Nextcloud supports [server-side encryption (SSE)](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html).
+    This is not an end-to-end encryption, but can be used to encrypt files that will be persisted
+    to external storage such as S3. Please note that this won't work anymore when using OpenSSL 3
+    for PHP's openssl extension and **Nextcloud 25 or older** because this is implemented using the
+    legacy cipher RC4. For Nextcloud26 this isn't relevant anymore, because Nextcloud has an RC4 implementation
+    written in native PHP and thus doesn't need `ext-openssl` for that anymore.
+    If [](#opt-system.stateVersion) is *above* `22.05`,
+    this is disabled by default. To turn it on again and for further information please refer to
+    [](#opt-services.nextcloud.enableBrokenCiphersForSSE).
+
+## Using an alternative webserver as reverse-proxy (e.g. `httpd`) {#module-services-nextcloud-httpd}
+
+By default, `nginx` is used as reverse-proxy for `nextcloud`.
+However, it's possible to use e.g. `httpd` by explicitly disabling
+`nginx` using [](#opt-services.nginx.enable) and fixing the
+settings `listen.owner` &amp; `listen.group` in the
+[corresponding `phpfpm` pool](#opt-services.phpfpm.pools).
+
+An exemplary configuration may look like this:
+```
+{ config, lib, pkgs, ... }: {
+  services.nginx.enable = false;
+  services.nextcloud = {
+    enable = true;
+    hostName = "localhost";
+
+    /* further, required options */
+  };
+  services.phpfpm.pools.nextcloud.settings = {
+    "listen.owner" = config.services.httpd.user;
+    "listen.group" = config.services.httpd.group;
+  };
+  services.httpd = {
+    enable = true;
+    adminAddr = "webmaster@localhost";
+    extraModules = [ "proxy_fcgi" ];
+    virtualHosts."localhost" = {
+      documentRoot = config.services.nextcloud.package;
+      extraConfig = ''
+        <Directory "${config.services.nextcloud.package}">
+          <FilesMatch "\.php$">
+            <If "-f %{REQUEST_FILENAME}">
+              SetHandler "proxy:unix:${config.services.phpfpm.pools.nextcloud.socket}|fcgi://localhost/"
+            </If>
+          </FilesMatch>
+          <IfModule mod_rewrite.c>
+            RewriteEngine On
+            RewriteBase /
+            RewriteRule ^index\.php$ - [L]
+            RewriteCond %{REQUEST_FILENAME} !-f
+            RewriteCond %{REQUEST_FILENAME} !-d
+            RewriteRule . /index.php [L]
+          </IfModule>
+          DirectoryIndex index.php
+          Require all granted
+          Options +FollowSymLinks
+        </Directory>
+      '';
+    };
+  };
+}
+```
+
+## Installing Apps and PHP extensions {#installing-apps-php-extensions-nextcloud}
+
+Nextcloud apps are installed statefully through the web interface.
+Some apps may require extra PHP extensions to be installed.
+This can be configured with the [](#opt-services.nextcloud.phpExtraExtensions) setting.
+
+Alternatively, extra apps can also be declared with the [](#opt-services.nextcloud.extraApps) setting.
+When using this setting, apps can no longer be managed statefully because this can lead to Nextcloud updating apps
+that are managed by Nix. If you want automatic updates it is recommended that you use web interface to install apps.
+
+## Maintainer information {#module-services-nextcloud-maintainer-info}
+
+As stated in the previous paragraph, we must provide a clean upgrade-path for Nextcloud
+since it cannot move more than one major version forward on a single upgrade. This chapter
+adds some notes how Nextcloud updates should be rolled out in the future.
+
+While minor and patch-level updates are no problem and can be done directly in the
+package-expression (and should be backported to supported stable branches after that),
+major-releases should be added in a new attribute (e.g. Nextcloud `v19.0.0`
+should be available in `nixpkgs` as `pkgs.nextcloud19`).
+To provide simple upgrade paths it's generally useful to backport those as well to stable
+branches. As long as the package-default isn't altered, this won't break existing setups.
+After that, the versioning-warning in the `nextcloud`-module should be
+updated to make sure that the
+[package](#opt-services.nextcloud.package)-option selects the latest version
+on fresh setups.
+
+If major-releases will be abandoned by upstream, we should check first if those are needed
+in NixOS for a safe upgrade-path before removing those. In that case we should keep those
+packages, but mark them as insecure in an expression like this (in
+`<nixpkgs/pkgs/servers/nextcloud/default.nix>`):
+```
+/* ... */
+{
+  nextcloud17 = generic {
+    version = "17.0.x";
+    sha256 = "0000000000000000000000000000000000000000000000000000";
+    eol = true;
+  };
+}
+```
+
+Ideally we should make sure that it's possible to jump two NixOS versions forward:
+i.e. the warnings and the logic in the module should guard a user to upgrade from a
+Nextcloud on e.g. 19.09 to a Nextcloud on 20.09.
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index 90801e99681..8edf270c889 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -57,6 +57,9 @@ let
 
   inherit (config.system) stateVersion;
 
+  mysqlLocal = cfg.database.createLocally && cfg.config.dbtype == "mysql";
+  pgsqlLocal = cfg.database.createLocally && cfg.config.dbtype == "pgsql";
+
 in {
 
   imports = [
@@ -79,7 +82,7 @@ in {
       (which can be opened e.g. by running `nixos-help`).
     '')
     (mkRemovedOptionModule [ "services" "nextcloud" "disableImagemagick" ] ''
-      Use services.nextcloud.nginx.enableImagemagick instead.
+      Use services.nextcloud.enableImagemagick instead.
     '')
   ];
 
@@ -204,7 +207,7 @@ in {
     package = mkOption {
       type = types.package;
       description = lib.mdDoc "Which package to use for the Nextcloud instance.";
-      relatedPackages = [ "nextcloud24" "nextcloud25" ];
+      relatedPackages = [ "nextcloud25" "nextcloud26" ];
     };
     phpPackage = mkOption {
       type = types.package;
@@ -314,13 +317,9 @@ in {
 
       createLocally = mkOption {
         type = types.bool;
-        default = false;
+        default = true;
         description = lib.mdDoc ''
-          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.
+          Create the database and database user locally.
         '';
       };
 
@@ -352,12 +351,15 @@ in {
       };
       dbhost = mkOption {
         type = types.nullOr types.str;
-        default = "localhost";
+        default =
+          if pgsqlLocal then "/run/postgresql"
+          else if mysqlLocal then "localhost:/run/mysqld/mysqld.sock"
+          else "localhost";
+        defaultText = "localhost";
         description = lib.mdDoc ''
-          Database host.
-
-          Note: for using Unix authentication with PostgreSQL, this should be
-          set to `/run/postgresql`.
+          Database host or socket path. Defaults to the correct unix socket
+          instead if `services.nextcloud.database.createLocally` is true and
+          `services.nextcloud.config.dbtype` is either `pgsql` or `mysql`.
         '';
       };
       dbport = mkOption {
@@ -514,6 +516,27 @@ in {
               `http://hostname.domain/bucket` instead.
             '';
           };
+          sseCKeyFile = mkOption {
+            type = types.nullOr types.path;
+            default = null;
+            example = "/var/nextcloud-objectstore-s3-sse-c-key";
+            description = lib.mdDoc ''
+              If provided this is the full path to a file that contains the key
+              to enable [server-side encryption with customer-provided keys][1]
+              (SSE-C).
+
+              The file must contain a random 32-byte key encoded as a base64
+              string, e.g. generated with the command
+
+              ```
+              openssl rand 32 | base64
+              ```
+
+              Must be readable by user `nextcloud`.
+
+              [1]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html
+            '';
+          };
         };
       };
     };
@@ -652,7 +675,7 @@ in {
 
   config = mkIf cfg.enable (mkMerge [
     { warnings = let
-        latest = 25;
+        latest = 26;
         upgradeWarning = major: nixos:
           ''
             A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
@@ -667,20 +690,6 @@ in {
             `services.nextcloud.package`.
           '';
 
-        # FIXME(@Ma27) remove as soon as nextcloud properly supports
-        # mariadb >=10.6.
-        isUnsupportedMariadb =
-          # All currently supported Nextcloud versions are affected (https://github.com/nextcloud/server/issues/25436).
-          (versionOlder cfg.package.version "24")
-          # This module uses mysql
-          && (cfg.config.dbtype == "mysql")
-          # MySQL is managed via NixOS
-          && config.services.mysql.enable
-          # We're using MariaDB
-          && (getName config.services.mysql.package) == "mariadb-server"
-          # MariaDB is at least 10.6 and thus not supported
-          && (versionAtLeast (getVersion config.services.mysql.package) "10.6");
-
       in (optional (cfg.poolConfig != null) ''
           Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release.
           Please migrate your configuration to config.services.nextcloud.poolSettings.
@@ -688,6 +697,7 @@ in {
         ++ (optional (versionOlder cfg.package.version "23") (upgradeWarning 22 "22.05"))
         ++ (optional (versionOlder cfg.package.version "24") (upgradeWarning 23 "22.05"))
         ++ (optional (versionOlder cfg.package.version "25") (upgradeWarning 24 "22.11"))
+        ++ (optional (versionOlder cfg.package.version "26") (upgradeWarning 25 "23.05"))
         ++ (optional cfg.enableBrokenCiphersForSSE ''
           You're using PHP's openssl extension built against OpenSSL 1.1 for Nextcloud.
           This is only necessary if you're using Nextcloud's server-side encryption.
@@ -705,17 +715,10 @@ in {
 
           For more context, here is the implementing pull request: https://github.com/NixOS/nixpkgs/pull/198470
         '')
-        ++ (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
-
-            * Switch to `pkgs.mysql`
-            * Downgrade MariaDB to at least 10.5
-            * Work around Nextcloud's problems by specifying `innodb_read_only_compressed=0`
-
-            For further context, please read
-            https://help.nextcloud.com/t/update-to-next-cloud-21-0-2-has-get-an-error/117028/15
-          '');
+        ++ (optional (cfg.enableBrokenCiphersForSSE && versionAtLeast cfg.package.version "26") ''
+          Nextcloud26 supports RC4 without requiring legacy OpenSSL, so
+          `services.nextcloud.enableBrokenCiphersForSSE` can be set to `false`.
+        '');
 
       services.nextcloud.package = with pkgs;
         mkDefault (
@@ -726,17 +729,32 @@ in {
               `pkgs.nextcloud`.
             ''
           else if versionOlder stateVersion "22.11" then nextcloud24
-          else nextcloud25
+          else if versionOlder stateVersion "23.05" then nextcloud25
+          else nextcloud26
         );
 
       services.nextcloud.phpPackage =
-        if versionOlder cfg.package.version "24" then pkgs.php80
-        else pkgs.php81;
+        if versionOlder cfg.package.version "26" then pkgs.php81
+        else pkgs.php82;
     }
 
     { 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.'';
+      { assertion = cfg.database.createLocally -> cfg.config.dbpassFile == null;
+        message = ''
+          Using `services.nextcloud.database.createLocally` (that now defaults
+          to true) with database password authentication is no longer
+          supported.
+
+          If you use an external database (or want to use password auth for any
+          other reason), set `services.nextcloud.database.createLocally` to
+          `false`. The database won't be managed for you (use `services.mysql`
+          if you want to set it up).
+
+          If you want this module to manage your nextcloud database for you,
+          unset `services.nextcloud.config.dbpassFile` and
+          `services.nextcloud.config.dbhost` to use socket authentication
+          instead of password.
+        '';
       }
     ]; }
 
@@ -773,6 +791,7 @@ in {
                 'use_ssl' => ${boolToString s3.useSsl},
                 ${optionalString (s3.region != null) "'region' => '${s3.region}',"}
                 'use_path_style' => ${boolToString s3.usePathStyle},
+                ${optionalString (s3.sseCKeyFile != null) "'sse_c_key' => nix_read_secret('${s3.sseCKeyFile}'),"}
               ],
             ]
           '';
@@ -899,6 +918,8 @@ in {
         in {
           wantedBy = [ "multi-user.target" ];
           before = [ "phpfpm-nextcloud.service" ];
+          after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
+          requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
           path = [ occ ];
           script = ''
             ${optionalString (c.dbpassFile != null) ''
@@ -958,6 +979,9 @@ in {
           '';
           serviceConfig.Type = "oneshot";
           serviceConfig.User = "nextcloud";
+          # On Nextcloud ≥ 26, it is not necessary to patch the database files to prevent
+          # an automatic creation of the database user.
+          environment.NC_setup_create_db_user = lib.mkIf (nextcloudGreaterOrEqualThan "26") "false";
         };
         nextcloud-cron = {
           after = [ "nextcloud-setup.service" ];
@@ -1001,7 +1025,7 @@ in {
 
       environment.systemPackages = [ occ ];
 
-      services.mysql = lib.mkIf cfg.database.createLocally {
+      services.mysql = lib.mkIf mysqlLocal {
         enable = true;
         package = lib.mkDefault pkgs.mariadb;
         ensureDatabases = [ cfg.config.dbname ];
@@ -1009,22 +1033,15 @@ in {
           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.postgresql = mkIf pgsqlLocal {
+        enable = true;
+        ensureDatabases = [ cfg.config.dbname ];
+        ensureUsers = [{
+          name = cfg.config.dbuser;
+          ensurePermissions = { "DATABASE ${cfg.config.dbname}" = "ALL PRIVILEGES"; };
+        }];
       };
 
       services.nginx.enable = mkDefault true;
@@ -1118,7 +1135,7 @@ in {
           ${optionalString (cfg.nginx.recommendedHttpHeaders) ''
             add_header X-Content-Type-Options nosniff;
             add_header X-XSS-Protection "1; mode=block";
-            add_header X-Robots-Tag none;
+            add_header X-Robots-Tag "noindex, nofollow";
             add_header X-Download-Options noopen;
             add_header X-Permitted-Cross-Domain-Policies none;
             add_header X-Frame-Options sameorigin;
@@ -1146,5 +1163,5 @@ in {
     }
   ]);
 
-  meta.doc = ./nextcloud.xml;
+  meta.doc = ./nextcloud.md;
 }
diff --git a/nixos/modules/services/web-apps/nextcloud.xml b/nixos/modules/services/web-apps/nextcloud.xml
deleted file mode 100644
index 4207c4008d5..00000000000
--- a/nixos/modules/services/web-apps/nextcloud.xml
+++ /dev/null
@@ -1,305 +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-nextcloud">
- <title>Nextcloud</title>
- <para>
-  <link xlink:href="https://nextcloud.com/">Nextcloud</link> is an open-source,
-  self-hostable cloud platform. The server setup can be automated using
-  <link linkend="opt-services.nextcloud.enable">services.nextcloud</link>. A
-  desktop client is packaged at <literal>pkgs.nextcloud-client</literal>.
- </para>
- <para>
-  The current default by NixOS is <package>nextcloud25</package> which is also the latest
-  major version available.
- </para>
- <section xml:id="module-services-nextcloud-basic-usage">
-  <title>Basic usage</title>
-
-  <para>
-   Nextcloud is a PHP-based application which requires an HTTP server
-   (<literal><link linkend="opt-services.nextcloud.enable">services.nextcloud</link></literal>
-   optionally supports
-   <literal><link linkend="opt-services.nginx.enable">services.nginx</link></literal>)
-   and a database (it's recommended to use
-   <literal><link linkend="opt-services.postgresql.enable">services.postgresql</link></literal>).
-  </para>
-
-  <para>
-   A very basic configuration may look like this:
-<programlisting>{ pkgs, ... }:
-{
-  services.nextcloud = {
-    <link linkend="opt-services.nextcloud.enable">enable</link> = true;
-    <link linkend="opt-services.nextcloud.hostName">hostName</link> = "nextcloud.tld";
-    config = {
-      <link linkend="opt-services.nextcloud.config.dbtype">dbtype</link> = "pgsql";
-      <link linkend="opt-services.nextcloud.config.dbuser">dbuser</link> = "nextcloud";
-      <link linkend="opt-services.nextcloud.config.dbhost">dbhost</link> = "/run/postgresql"; # nextcloud will add /.s.PGSQL.5432 by itself
-      <link linkend="opt-services.nextcloud.config.dbname">dbname</link> = "nextcloud";
-      <link linkend="opt-services.nextcloud.config.adminpassFile">adminpassFile</link> = "/path/to/admin-pass-file";
-      <link linkend="opt-services.nextcloud.config.adminuser">adminuser</link> = "root";
-    };
-  };
-
-  services.postgresql = {
-    <link linkend="opt-services.postgresql.enable">enable</link> = true;
-    <link linkend="opt-services.postgresql.ensureDatabases">ensureDatabases</link> = [ "nextcloud" ];
-    <link linkend="opt-services.postgresql.ensureUsers">ensureUsers</link> = [
-     { name = "nextcloud";
-       ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
-     }
-    ];
-  };
-
-  # ensure that postgres is running *before* running the setup
-  systemd.services."nextcloud-setup" = {
-    requires = ["postgresql.service"];
-    after = ["postgresql.service"];
-  };
-
-  <link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 80 443 ];
-}</programlisting>
-  </para>
-
-  <para>
-   The <literal>hostName</literal> option is used internally to configure an HTTP
-   server using <literal><link xlink:href="https://php-fpm.org/">PHP-FPM</link></literal>
-   and <literal>nginx</literal>. The <literal>config</literal> attribute set is
-   used by the imperative installer and all values are written to an additional file
-   to ensure that changes can be applied by changing the module's options.
-  </para>
-
-  <para>
-   In case the application serves multiple domains (those are checked with
-   <literal><link xlink:href="http://php.net/manual/en/reserved.variables.server.php">$_SERVER['HTTP_HOST']</link></literal>)
-   it's needed to add them to
-   <literal><link linkend="opt-services.nextcloud.config.extraTrustedDomains">services.nextcloud.config.extraTrustedDomains</link></literal>.
-  </para>
-
-  <para>
-   Auto updates for Nextcloud apps can be enabled using
-   <literal><link linkend="opt-services.nextcloud.autoUpdateApps.enable">services.nextcloud.autoUpdateApps</link></literal>.
-</para>
-
- </section>
-
- <section xml:id="module-services-nextcloud-pitfalls-during-upgrade">
-  <title>Common problems</title>
-  <itemizedlist>
-   <listitem>
-    <formalpara>
-     <title>General notes</title>
-     <para>
-      Unfortunately Nextcloud appears to be very stateful when it comes to
-      managing its own configuration. The config file lives in the home directory
-      of the <literal>nextcloud</literal> user (by default
-      <literal>/var/lib/nextcloud/config/config.php</literal>) and is also used to
-      track several states of the application (e.g., whether installed or not).
-     </para>
-    </formalpara>
-    <para>
-     All configuration parameters are also stored in
-     <filename>/var/lib/nextcloud/config/override.config.php</filename> which is generated by
-     the module and linked from the store to ensure that all values from
-     <filename>config.php</filename> can be modified by the module.
-     However <filename>config.php</filename> manages the application's state and shouldn't be
-     touched manually because of that.
-    </para>
-    <warning>
-     <para>Don't delete <filename>config.php</filename>! This file
-     tracks the application's state and a deletion can cause unwanted
-     side-effects!</para>
-    </warning>
-
-    <warning>
-     <para>Don't rerun <literal>nextcloud-occ
-     maintenance:install</literal>! This command tries to install the application
-     and can cause unwanted side-effects!</para>
-    </warning>
-   </listitem>
-   <listitem>
-    <formalpara>
-     <title>Multiple version upgrades</title>
-     <para>
-      Nextcloud doesn't allow to move more than one major-version forward. E.g., if you're on
-      <literal>v16</literal>, you cannot upgrade to <literal>v18</literal>, you need to upgrade to
-      <literal>v17</literal> first. This is ensured automatically as long as the
-      <link linkend="opt-system.stateVersion">stateVersion</link> is declared properly. In that case
-      the oldest version available (one major behind the one from the previous NixOS
-      release) will be selected by default and the module will generate a warning that reminds
-      the user to upgrade to latest Nextcloud <emphasis>after</emphasis> that deploy.
-     </para>
-    </formalpara>
-   </listitem>
-   <listitem>
-    <formalpara>
-     <title><literal>Error: Command "upgrade" is not defined.</literal></title>
-     <para>
-      This error usually occurs if the initial installation
-      (<command>nextcloud-occ maintenance:install</command>) has failed. After that, the application
-      is not installed, but the upgrade is attempted to be executed. Further context can
-      be found in <link xlink:href="https://github.com/NixOS/nixpkgs/issues/111175">NixOS/nixpkgs#111175</link>.
-     </para>
-    </formalpara>
-    <para>
-     First of all, it makes sense to find out what went wrong by looking at the logs
-     of the installation via <command>journalctl -u nextcloud-setup</command> and try to fix
-     the underlying issue.
-    </para>
-    <itemizedlist>
-     <listitem>
-      <para>
-       If this occurs on an <emphasis>existing</emphasis> setup, this is most likely because
-       the maintenance mode is active. It can be deactivated by running
-       <command>nextcloud-occ maintenance:mode --off</command>. It's advisable though to
-       check the logs first on why the maintenance mode was activated.
-      </para>
-     </listitem>
-     <listitem>
-      <warning><para>Only perform the following measures on
-      <emphasis>freshly installed instances!</emphasis></para></warning>
-      <para>
-       A re-run of the installer can be forced by <emphasis>deleting</emphasis>
-       <filename>/var/lib/nextcloud/config/config.php</filename>. This is the only time
-       advisable because the fresh install doesn't have any state that can be lost.
-       In case that doesn't help, an entire re-creation can be forced via
-       <command>rm -rf ~nextcloud/</command>.
-      </para>
-     </listitem>
-    </itemizedlist>
-   </listitem>
-   <listitem>
-    <formalpara>
-     <title>Server-side encryption</title>
-     <para>
-      Nextcloud supports <link xlink:href="https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html">server-side encryption (SSE)</link>.
-      This is not an end-to-end encryption, but can be used to encrypt files that will be persisted
-      to external storage such as S3. Please note that this won't work anymore when using OpenSSL 3
-      for PHP's openssl extension because this is implemented using the legacy cipher RC4.
-      If <xref linkend="opt-system.stateVersion" /> is <emphasis>above</emphasis> <literal>22.05</literal>,
-      this is disabled by default. To turn it on again and for further information please refer to
-      <xref linkend="opt-services.nextcloud.enableBrokenCiphersForSSE" />.
-     </para>
-    </formalpara>
-   </listitem>
-  </itemizedlist>
- </section>
-
- <section xml:id="module-services-nextcloud-httpd">
-  <title>Using an alternative webserver as reverse-proxy (e.g. <literal>httpd</literal>)</title>
-  <para>
-   By default, <package>nginx</package> is used as reverse-proxy for <package>nextcloud</package>.
-   However, it's possible to use e.g. <package>httpd</package> by explicitly disabling
-   <package>nginx</package> using <xref linkend="opt-services.nginx.enable" /> and fixing the
-   settings <literal>listen.owner</literal> &amp; <literal>listen.group</literal> in the
-   <link linkend="opt-services.phpfpm.pools">corresponding <literal>phpfpm</literal> pool</link>.
-  </para>
-  <para>
-   An exemplary configuration may look like this:
-<programlisting>{ config, lib, pkgs, ... }: {
-  <link linkend="opt-services.nginx.enable">services.nginx.enable</link> = false;
-  services.nextcloud = {
-    <link linkend="opt-services.nextcloud.enable">enable</link> = true;
-    <link linkend="opt-services.nextcloud.hostName">hostName</link> = "localhost";
-
-    /* further, required options */
-  };
-  <link linkend="opt-services.phpfpm.pools._name_.settings">services.phpfpm.pools.nextcloud.settings</link> = {
-    "listen.owner" = config.services.httpd.user;
-    "listen.group" = config.services.httpd.group;
-  };
-  services.httpd = {
-    <link linkend="opt-services.httpd.enable">enable</link> = true;
-    <link linkend="opt-services.httpd.adminAddr">adminAddr</link> = "webmaster@localhost";
-    <link linkend="opt-services.httpd.extraModules">extraModules</link> = [ "proxy_fcgi" ];
-    virtualHosts."localhost" = {
-      <link linkend="opt-services.httpd.virtualHosts._name_.documentRoot">documentRoot</link> = config.services.nextcloud.package;
-      <link linkend="opt-services.httpd.virtualHosts._name_.extraConfig">extraConfig</link> = ''
-        &lt;Directory "${config.services.nextcloud.package}"&gt;
-          &lt;FilesMatch "\.php$"&gt;
-            &lt;If "-f %{REQUEST_FILENAME}"&gt;
-              SetHandler "proxy:unix:${config.services.phpfpm.pools.nextcloud.socket}|fcgi://localhost/"
-            &lt;/If&gt;
-          &lt;/FilesMatch&gt;
-          &lt;IfModule mod_rewrite.c&gt;
-            RewriteEngine On
-            RewriteBase /
-            RewriteRule ^index\.php$ - [L]
-            RewriteCond %{REQUEST_FILENAME} !-f
-            RewriteCond %{REQUEST_FILENAME} !-d
-            RewriteRule . /index.php [L]
-          &lt;/IfModule&gt;
-          DirectoryIndex index.php
-          Require all granted
-          Options +FollowSymLinks
-        &lt;/Directory&gt;
-      '';
-    };
-  };
-}</programlisting>
-  </para>
- </section>
-
- <section xml:id="installing-apps-php-extensions-nextcloud">
-  <title>Installing Apps and PHP extensions</title>
-
-  <para>
-   Nextcloud apps are installed statefully through the web interface.
-
-   Some apps may require extra PHP extensions to be installed.
-   This can be configured with the <xref linkend="opt-services.nextcloud.phpExtraExtensions" /> setting.
-  </para>
-
-  <para>
-   Alternatively, extra apps can also be declared with the <xref linkend="opt-services.nextcloud.extraApps" /> setting.
-   When using this setting, apps can no longer be managed statefully because this can lead to Nextcloud updating apps
-   that are managed by Nix. If you want automatic updates it is recommended that you use web interface to install apps.
-  </para>
- </section>
-
- <section xml:id="module-services-nextcloud-maintainer-info">
-  <title>Maintainer information</title>
-
-  <para>
-   As stated in the previous paragraph, we must provide a clean upgrade-path for Nextcloud
-   since it cannot move more than one major version forward on a single upgrade. This chapter
-   adds some notes how Nextcloud updates should be rolled out in the future.
-  </para>
-
-  <para>
-   While minor and patch-level updates are no problem and can be done directly in the
-   package-expression (and should be backported to supported stable branches after that),
-   major-releases should be added in a new attribute (e.g. Nextcloud <literal>v19.0.0</literal>
-   should be available in <literal>nixpkgs</literal> as <literal>pkgs.nextcloud19</literal>).
-   To provide simple upgrade paths it's generally useful to backport those as well to stable
-   branches. As long as the package-default isn't altered, this won't break existing setups.
-   After that, the versioning-warning in the <literal>nextcloud</literal>-module should be
-   updated to make sure that the
-   <link linkend="opt-services.nextcloud.package">package</link>-option selects the latest version
-   on fresh setups.
-  </para>
-
-  <para>
-   If major-releases will be abandoned by upstream, we should check first if those are needed
-   in NixOS for a safe upgrade-path before removing those. In that case we should keep those
-   packages, but mark them as insecure in an expression like this (in
-   <literal>&lt;nixpkgs/pkgs/servers/nextcloud/default.nix&gt;</literal>):
-<programlisting>/* ... */
-{
-  nextcloud17 = generic {
-    version = "17.0.x";
-    sha256 = "0000000000000000000000000000000000000000000000000000";
-    eol = true;
-  };
-}</programlisting>
-  </para>
-
-  <para>
-   Ideally we should make sure that it's possible to jump two NixOS versions forward:
-   i.e. the warnings and the logic in the module should guard a user to upgrade from a
-   Nextcloud on e.g. 19.09 to a Nextcloud on 20.09.
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/onlyoffice.nix b/nixos/modules/services/web-apps/onlyoffice.nix
index 79ed3e43dd1..3494f2fa21f 100644
--- a/nixos/modules/services/web-apps/onlyoffice.nix
+++ b/nixos/modules/services/web-apps/onlyoffice.nix
@@ -229,6 +229,9 @@ in
             cp -r ${cfg.package}/etc/onlyoffice/documentserver/* /run/onlyoffice/config/
             chmod u+w /run/onlyoffice/config/default.json
 
+            # Allow members of the onlyoffice group to serve files under /var/lib/onlyoffice/documentserver/App_Data
+            chmod g+x /var/lib/onlyoffice/documentserver
+
             cp /run/onlyoffice/config/default.json{,.orig}
 
             # for a mapping of environment variables from the docker container to json options see
@@ -267,7 +270,7 @@ in
           wantedBy = [ "multi-user.target" ];
           serviceConfig = {
             ExecStart = "${cfg.package.fhs}/bin/onlyoffice-wrapper DocService/docservice /run/onlyoffice/config";
-            ExecStartPre = onlyoffice-prestart;
+            ExecStartPre = [ onlyoffice-prestart ];
             Group = "onlyoffice";
             Restart = "always";
             RuntimeDirectory = "onlyoffice";
@@ -284,6 +287,8 @@ in
         group = "onlyoffice";
         isSystemUser = true;
       };
+
+      nginx.extraGroups = [ "onlyoffice" ];
     };
 
     users.groups.onlyoffice = { };
diff --git a/nixos/modules/services/web-apps/peertube.nix b/nixos/modules/services/web-apps/peertube.nix
index 4dbcb09d2ae..65b3b70c48a 100644
--- a/nixos/modules/services/web-apps/peertube.nix
+++ b/nixos/modules/services/web-apps/peertube.nix
@@ -161,6 +161,18 @@ in {
       description = lib.mdDoc "Configure nginx as a reverse proxy for peertube.";
     };
 
+    secrets = {
+      secretsFile = lib.mkOption {
+        type = lib.types.nullOr lib.types.path;
+        default = null;
+        example = "/run/secrets/peertube";
+        description = lib.mdDoc ''
+          Secrets to run PeerTube.
+          Generate one using `openssl rand -hex 32`
+        '';
+      };
+    };
+
     database = {
       createLocally = lib.mkOption {
         type = lib.types.bool;
@@ -201,7 +213,7 @@ in {
       passwordFile = lib.mkOption {
         type = lib.types.nullOr lib.types.path;
         default = null;
-        example = "/run/keys/peertube/password-posgressql-db";
+        example = "/run/keys/peertube/password-postgresql";
         description = lib.mdDoc "Password for PostgreSQL database.";
       };
     };
@@ -282,6 +294,11 @@ in {
             prevent this.
           '';
       }
+      { assertion = cfg.secrets.secretsFile != null;
+          message = ''
+            <option>services.peertube.secrets.secretsFile</option> needs to be set.
+          '';
+      }
       { assertion = !(cfg.redis.enableUnixSocket && (cfg.redis.host != null || cfg.redis.port != null));
           message = ''
             <option>services.peertube.redis.createLocally</option> and redis network connection (<option>services.peertube.redis.host</option> or <option>services.peertube.redis.port</option>) enabled. Disable either of them.
@@ -349,6 +366,7 @@ in {
           captions = lib.mkDefault "/var/lib/peertube/storage/captions/";
           cache = lib.mkDefault "/var/lib/peertube/storage/cache/";
           plugins = lib.mkDefault "/var/lib/peertube/storage/plugins/";
+          well_known = lib.mkDefault "/var/lib/peertube/storage/well_known/";
           client_overrides = lib.mkDefault "/var/lib/peertube/storage/client-overrides/";
         };
         import = {
@@ -411,12 +429,16 @@ in {
 
       environment = env;
 
-      path = with pkgs; [ bashInteractive ffmpeg nodejs-16_x openssl yarn python3 ];
+      path = with pkgs; [ bashInteractive ffmpeg nodejs_16 openssl yarn python3 ];
 
       script = ''
         #!/bin/sh
         umask 077
         cat > /var/lib/peertube/config/local.yaml <<EOF
+        ${lib.optionalString (cfg.secrets.secretsFile != null) ''
+        secrets:
+          peertube: '$(cat ${cfg.secrets.secretsFile})'
+        ''}
         ${lib.optionalString ((!cfg.database.createLocally) && (cfg.database.passwordFile != null)) ''
         database:
           password: '$(cat ${cfg.database.passwordFile})'
@@ -443,6 +465,7 @@ in {
         RestartSec = 20;
         TimeoutSec = 60;
         WorkingDirectory = cfg.package;
+        SyslogIdentifier = "peertube";
         # User and group
         User = cfg.user;
         Group = cfg.group;
@@ -548,9 +571,14 @@ in {
           '';
         };
 
+        locations."~ ^/plugins/[^/]+(/[^/]+)?/ws/" = {
+          tryFiles = "/dev/null @api_websocket";
+          priority = 1230;
+        };
+
         locations."@api_websocket" = {
           proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
-          priority = 1230;
+          priority = 1240;
 
           extraConfig = ''
             proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
@@ -581,7 +609,7 @@ in {
           '';
         };
 
-        locations."~ ^/lazy-static/(avatars|banners)/" = {
+        locations."^~ /lazy-static/avatars/" = {
           tryFiles = "$uri @api";
           root = cfg.settings.storage.avatars;
           priority = 1330;
@@ -599,6 +627,26 @@ in {
             add_header Cache-Control                    'public, max-age=7200';
 
             rewrite ^/lazy-static/avatars/(.*)$         /$1 break;
+          '';
+        };
+
+        locations."^~ /lazy-static/banners/" = {
+          tryFiles = "$uri @api";
+          root = cfg.settings.storage.avatars;
+          priority = 1340;
+          extraConfig = ''
+            if ($request_method = 'OPTIONS') {
+              ${nginxCommonHeaders}
+              add_header Access-Control-Max-Age         1728000;
+              add_header Cache-Control                  'no-cache';
+              add_header Content-Type                   'text/plain charset=UTF-8';
+              add_header Content-Length                 0;
+              return                                    204;
+            }
+
+            ${nginxCommonHeaders}
+            add_header Cache-Control                    'public, max-age=7200';
+
             rewrite ^/lazy-static/banners/(.*)$         /$1 break;
           '';
         };
@@ -606,7 +654,7 @@ in {
         locations."^~ /lazy-static/previews/" = {
           tryFiles = "$uri @api";
           root = cfg.settings.storage.previews;
-          priority = 1340;
+          priority = 1350;
           extraConfig = ''
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
@@ -624,10 +672,34 @@ in {
           '';
         };
 
+        locations."^~ /static/streaming-playlists/private/" = {
+          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          priority = 1410;
+          extraConfig = ''
+            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
+            proxy_set_header Host                       $host;
+            proxy_set_header X-Real-IP                  $remote_addr;
+
+            proxy_limit_rate                            5M;
+          '';
+        };
+
+        locations."^~ /static/webseed/private/" = {
+          proxyPass = "http://127.0.0.1:${toString cfg.listenHttp}";
+          priority = 1420;
+          extraConfig = ''
+            proxy_set_header X-Forwarded-For            $proxy_add_x_forwarded_for;
+            proxy_set_header Host                       $host;
+            proxy_set_header X-Real-IP                  $remote_addr;
+
+            proxy_limit_rate                            5M;
+          '';
+        };
+
         locations."^~ /static/thumbnails/" = {
           tryFiles = "$uri @api";
           root = cfg.settings.storage.thumbnails;
-          priority = 1350;
+          priority = 1430;
           extraConfig = ''
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
@@ -648,8 +720,14 @@ in {
         locations."^~ /static/redundancy/" = {
           tryFiles = "$uri @api";
           root = cfg.settings.storage.redundancy;
-          priority = 1360;
+          priority = 1440;
           extraConfig = ''
+            set $peertube_limit_rate                    800k;
+
+            if ($request_uri ~ -fragmented.mp4$) {
+              set $peertube_limit_rate                  5M;
+            }
+
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
               add_header Access-Control-Max-Age         1728000;
@@ -662,15 +740,14 @@ in {
 
               access_log                                off;
             }
+
             aio                                         threads;
             sendfile                                    on;
             sendfile_max_chunk                          1M;
 
+            limit_rate                                  $peertube_limit_rate;
             limit_rate_after                            5M;
 
-            set $peertube_limit_rate                    800k;
-            set $limit_rate                             $peertube_limit_rate;
-
             rewrite ^/static/redundancy/(.*)$           /$1 break;
           '';
         };
@@ -678,8 +755,14 @@ in {
         locations."^~ /static/streaming-playlists/" = {
           tryFiles = "$uri @api";
           root = cfg.settings.storage.streaming_playlists;
-          priority = 1370;
+          priority = 1450;
           extraConfig = ''
+            set $peertube_limit_rate                    800k;
+
+            if ($request_uri ~ -fragmented.mp4$) {
+              set $peertube_limit_rate                  5M;
+            }
+
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
               add_header Access-Control-Max-Age         1728000;
@@ -697,20 +780,24 @@ in {
             sendfile                                    on;
             sendfile_max_chunk                          1M;
 
+            limit_rate                                  $peertube_limit_rate;
             limit_rate_after                            5M;
 
-            set $peertube_limit_rate                    5M;
-            set $limit_rate                             $peertube_limit_rate;
-
             rewrite ^/static/streaming-playlists/(.*)$  /$1 break;
           '';
         };
 
-        locations."~ ^/static/webseed/" = {
+        locations."^~ /static/webseed/" = {
           tryFiles = "$uri @api";
           root = cfg.settings.storage.videos;
-          priority = 1380;
+          priority = 1460;
           extraConfig = ''
+            set $peertube_limit_rate                    800k;
+
+            if ($request_uri ~ -fragmented.mp4$) {
+              set $peertube_limit_rate                  5M;
+            }
+
             if ($request_method = 'OPTIONS') {
               ${nginxCommonHeaders}
               add_header Access-Control-Max-Age         1728000;
@@ -728,11 +815,9 @@ in {
             sendfile                                    on;
             sendfile_max_chunk                          1M;
 
+            limit_rate                                  $peertube_limit_rate;
             limit_rate_after                            5M;
 
-            set $peertube_limit_rate                    800k;
-            set $limit_rate                             $peertube_limit_rate;
-
             rewrite ^/static/webseed/(.*)$              /$1 break;
           '';
         };
@@ -774,7 +859,7 @@ in {
           home = cfg.package;
         };
       })
-      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package peertubeEnv peertubeCli pkgs.ffmpeg pkgs.nodejs-16_x pkgs.yarn ])
+      (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package peertubeEnv peertubeCli pkgs.ffmpeg pkgs.nodejs_16 pkgs.yarn ])
       (lib.mkIf cfg.redis.enableUnixSocket {${config.services.peertube.user}.extraGroups = [ "redis-peertube" ];})
     ];
 
diff --git a/nixos/modules/services/web-apps/photoprism.nix b/nixos/modules/services/web-apps/photoprism.nix
new file mode 100644
index 00000000000..d5ca6014780
--- /dev/null
+++ b/nixos/modules/services/web-apps/photoprism.nix
@@ -0,0 +1,155 @@
+{ config, pkgs, lib, ... }:
+let
+  cfg = config.services.photoprism;
+
+  env = {
+    PHOTOPRISM_ORIGINALS_PATH = cfg.originalsPath;
+    PHOTOPRISM_STORAGE_PATH = cfg.storagePath;
+    PHOTOPRISM_IMPORT_PATH = cfg.importPath;
+    PHOTOPRISM_HTTP_HOST = cfg.address;
+    PHOTOPRISM_HTTP_PORT = toString cfg.port;
+  } // (
+    lib.mapAttrs (_: toString) cfg.settings
+  );
+
+  manage =
+    let
+      setupEnv = lib.concatStringsSep "\n" (lib.mapAttrsToList (name: val: "export ${name}=${lib.escapeShellArg val}") env);
+    in
+    pkgs.writeShellScript "manage" ''
+      ${setupEnv}
+      exec ${cfg.package}/bin/photoprism "$@"
+    '';
+in
+{
+  meta.maintainers = with lib.maintainers; [ stunkymonkey ];
+
+  options.services.photoprism = {
+
+    enable = lib.mkEnableOption (lib.mdDoc "Photoprism web server");
+
+    passwordFile = lib.mkOption {
+      type = lib.types.nullOr lib.types.path;
+      default = null;
+      description = lib.mdDoc ''
+        Admin password file.
+      '';
+    };
+
+    address = lib.mkOption {
+      type = lib.types.str;
+      default = "localhost";
+      description = lib.mdDoc ''
+        Web interface address.
+      '';
+    };
+
+    port = lib.mkOption {
+      type = lib.types.port;
+      default = 2342;
+      description = lib.mdDoc ''
+        Web interface port.
+      '';
+    };
+
+    originalsPath = lib.mkOption {
+      type = lib.types.path;
+      default = null;
+      example = "/data/photos";
+      description = lib.mdDoc ''
+        Storage path of your original media files (photos and videos).
+      '';
+    };
+
+    importPath = lib.mkOption {
+      type = lib.types.str;
+      default = "import";
+      description = lib.mdDoc ''
+        Relative or absolute to the `originalsPath` from where the files should be imported.
+      '';
+    };
+
+    storagePath = lib.mkOption {
+      type = lib.types.path;
+      default = "/var/lib/photoprism";
+      description = lib.mdDoc ''
+        Location for sidecar, cache, and database files.
+      '';
+    };
+
+    package = lib.mkPackageOptionMD pkgs "photoprism" { };
+
+    settings = lib.mkOption {
+      type = lib.types.attrsOf lib.types.str;
+      default = { };
+      description = lib.mdDoc ''
+        See [the getting-started guide](https://docs.photoprism.app/getting-started/config-options/) for available options.
+      '';
+      example = {
+        PHOTOPRISM_DEFAULT_LOCALE = "de";
+        PHOTOPRISM_ADMIN_USER = "root";
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.photoprism = {
+      description = "Photoprism server";
+
+      serviceConfig = {
+        Restart = "on-failure";
+        User = "photoprism";
+        Group = "photoprism";
+        DynamicUser = true;
+        StateDirectory = "photoprism";
+        WorkingDirectory = "/var/lib/photoprism";
+        RuntimeDirectory = "photoprism";
+
+        LoadCredential = lib.optionalString (cfg.passwordFile != null)
+          "PHOTOPRISM_ADMIN_PASSWORD:${cfg.passwordFile}";
+
+        CapabilityBoundingSet = "";
+        LockPersonality = true;
+        PrivateDevices = true;
+        PrivateUsers = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelLogs = true;
+        ProtectKernelModules = true;
+        ProtectKernelTunables = true;
+        RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = [ "@system-service" "~@privileged @setuid @keyring" ];
+        UMask = "0066";
+      } // lib.optionalAttrs (cfg.port < 1024) {
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+      };
+
+      wantedBy = [ "multi-user.target" ];
+      environment = env;
+
+      # reminder: easier password configuration will come in https://github.com/photoprism/photoprism/pull/2302
+      preStart = ''
+        ln -sf ${manage} photoprism-manage
+
+        ${lib.optionalString (cfg.passwordFile != null) ''
+          export PHOTOPRISM_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PHOTOPRISM_ADMIN_PASSWORD")
+        ''}
+        exec ${cfg.package}/bin/photoprism migrations run -f
+      '';
+
+      script = ''
+        ${lib.optionalString (cfg.passwordFile != null) ''
+          export PHOTOPRISM_ADMIN_PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/PHOTOPRISM_ADMIN_PASSWORD")
+        ''}
+        exec ${cfg.package}/bin/photoprism start
+      '';
+    };
+  };
+}
+
diff --git a/nixos/modules/services/web-apps/pict-rs.md b/nixos/modules/services/web-apps/pict-rs.md
index 4b622049909..2fa6bb3aebc 100644
--- a/nixos/modules/services/web-apps/pict-rs.md
+++ b/nixos/modules/services/web-apps/pict-rs.md
@@ -15,6 +15,7 @@ this will start the http server on port 8080 by default.
 ## Usage {#module-services-pict-rs-usage}
 
 pict-rs offers the following endpoints:
+
 - `POST /image` for uploading an image. Uploaded content must be valid multipart/form-data with an
     image array located within the `images[]` key
 
diff --git a/nixos/modules/services/web-apps/pict-rs.nix b/nixos/modules/services/web-apps/pict-rs.nix
index ee9ff9b484f..0f13b2ae6db 100644
--- a/nixos/modules/services/web-apps/pict-rs.nix
+++ b/nixos/modules/services/web-apps/pict-rs.nix
@@ -5,9 +5,7 @@ let
 in
 {
   meta.maintainers = with maintainers; [ happysalada ];
-  # Don't edit the docbook xml directly, edit the md and generate it:
-  # `pandoc pict-rs.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > pict-rs.xml`
-  meta.doc = ./pict-rs.xml;
+  meta.doc = ./pict-rs.md;
 
   options.services.pict-rs = {
     enable = mkEnableOption (lib.mdDoc "pict-rs server");
diff --git a/nixos/modules/services/web-apps/pict-rs.xml b/nixos/modules/services/web-apps/pict-rs.xml
deleted file mode 100644
index bf129f5cc2a..00000000000
--- a/nixos/modules/services/web-apps/pict-rs.xml
+++ /dev/null
@@ -1,162 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="module-services-pict-rs">
-  <title>Pict-rs</title>
-  <para>
-    pict-rs is a a simple image hosting service.
-  </para>
-  <section xml:id="module-services-pict-rs-quickstart">
-    <title>Quickstart</title>
-    <para>
-      the minimum to start pict-rs is
-    </para>
-    <programlisting language="bash">
-services.pict-rs.enable = true;
-</programlisting>
-    <para>
-      this will start the http server on port 8080 by default.
-    </para>
-  </section>
-  <section xml:id="module-services-pict-rs-usage">
-    <title>Usage</title>
-    <para>
-      pict-rs offers the following endpoints: -
-      <literal>POST /image</literal> for uploading an image. Uploaded
-      content must be valid multipart/form-data with an image array
-      located within the <literal>images[]</literal> key
-    </para>
-    <programlisting>
-This endpoint returns the following JSON structure on success with a 201 Created status
-```json
-{
-    &quot;files&quot;: [
-        {
-            &quot;delete_token&quot;: &quot;JFvFhqJA98&quot;,
-            &quot;file&quot;: &quot;lkWZDRvugm.jpg&quot;
-        },
-        {
-            &quot;delete_token&quot;: &quot;kAYy9nk2WK&quot;,
-            &quot;file&quot;: &quot;8qFS0QooAn.jpg&quot;
-        },
-        {
-            &quot;delete_token&quot;: &quot;OxRpM3sf0Y&quot;,
-            &quot;file&quot;: &quot;1hJaYfGE01.jpg&quot;
-        }
-    ],
-    &quot;msg&quot;: &quot;ok&quot;
-}
-```
-</programlisting>
-    <itemizedlist>
-      <listitem>
-        <para>
-          <literal>GET /image/download?url=...</literal> Download an
-          image from a remote server, returning the same JSON payload as
-          the <literal>POST</literal> endpoint
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>GET /image/original/{file}</literal> for getting a
-          full-resolution image. <literal>file</literal> here is the
-          <literal>file</literal> key from the <literal>/image</literal>
-          endpoint’s JSON
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>GET /image/details/original/{file}</literal> for
-          getting the details of a full-resolution image. The returned
-          JSON is structured like so:
-          <literal>json     {         &quot;width&quot;: 800,         &quot;height&quot;: 537,         &quot;content_type&quot;: &quot;image/webp&quot;,         &quot;created_at&quot;: [             2020,             345,             67376,             394363487         ]     }</literal>
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>GET /image/process.{ext}?src={file}&amp;...</literal>
-          get a file with transformations applied. existing
-          transformations include
-        </para>
-        <itemizedlist spacing="compact">
-          <listitem>
-            <para>
-              <literal>identity=true</literal>: apply no changes
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>blur={float}</literal>: apply a gaussian blur to
-              the file
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>thumbnail={int}</literal>: produce a thumbnail of
-              the image fitting inside an <literal>{int}</literal> by
-              <literal>{int}</literal> square using raw pixel sampling
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>resize={int}</literal>: produce a thumbnail of
-              the image fitting inside an <literal>{int}</literal> by
-              <literal>{int}</literal> square using a Lanczos2 filter.
-              This is slower than sampling but looks a bit better in
-              some cases
-            </para>
-          </listitem>
-          <listitem>
-            <para>
-              <literal>crop={int-w}x{int-h}</literal>: produce a cropped
-              version of the image with an <literal>{int-w}</literal> by
-              <literal>{int-h}</literal> aspect ratio. The resulting
-              crop will be centered on the image. Either the width or
-              height of the image will remain full-size, depending on
-              the image’s aspect ratio and the requested aspect ratio.
-              For example, a 1600x900 image cropped with a 1x1 aspect
-              ratio will become 900x900. A 1600x1100 image cropped with
-              a 16x9 aspect ratio will become 1600x900.
-            </para>
-          </listitem>
-        </itemizedlist>
-        <para>
-          Supported <literal>ext</literal> file extensions include
-          <literal>png</literal>, <literal>jpg</literal>, and
-          <literal>webp</literal>
-        </para>
-        <para>
-          An example of usage could be
-          <literal>GET /image/process.jpg?src=asdf.png&amp;thumbnail=256&amp;blur=3.0</literal>
-          which would create a 256x256px JPEG thumbnail and blur it
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>GET /image/details/process.{ext}?src={file}&amp;...</literal>
-          for getting the details of a processed image. The returned
-          JSON is the same format as listed for the full-resolution
-          details endpoint.
-        </para>
-      </listitem>
-      <listitem>
-        <para>
-          <literal>DELETE /image/delete/{delete_token}/{file}</literal>
-          or <literal>GET /image/delete/{delete_token}/{file}</literal>
-          to delete a file, where <literal>delete_token</literal> and
-          <literal>file</literal> are from the <literal>/image</literal>
-          endpoint’s JSON
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-  <section xml:id="module-services-pict-rs-missing">
-    <title>Missing</title>
-    <itemizedlist spacing="compact">
-      <listitem>
-        <para>
-          Configuring the secure-api-key is not included yet. The
-          envisioned basic use case is consumption on localhost by other
-          services without exposing the service to the internet.
-        </para>
-      </listitem>
-    </itemizedlist>
-  </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/plausible.md b/nixos/modules/services/web-apps/plausible.md
new file mode 100644
index 00000000000..1328ce69441
--- /dev/null
+++ b/nixos/modules/services/web-apps/plausible.md
@@ -0,0 +1,35 @@
+# Plausible {#module-services-plausible}
+
+[Plausible](https://plausible.io/) is a privacy-friendly alternative to
+Google analytics.
+
+## Basic Usage {#module-services-plausible-basic-usage}
+
+At first, a secret key is needed to be generated. This can be done with e.g.
+```ShellSession
+$ openssl rand -base64 64
+```
+
+After that, `plausible` can be deployed like this:
+```
+{
+  services.plausible = {
+    enable = true;
+    adminUser = {
+      # activate is used to skip the email verification of the admin-user that's
+      # automatically created by plausible. This is only supported if
+      # postgresql is configured by the module. This is done by default, but
+      # can be turned off with services.plausible.database.postgres.setup.
+      activate = true;
+      email = "admin@localhost";
+      passwordFile = "/run/secrets/plausible-admin-pwd";
+    };
+    server = {
+      baseUrl = "http://analytics.example.org";
+      # secretKeybaseFile is a path to the file which contains the secret generated
+      # with openssl as described above.
+      secretKeybaseFile = "/run/secrets/plausible-secret-key-base";
+    };
+  };
+}
+```
diff --git a/nixos/modules/services/web-apps/plausible.nix b/nixos/modules/services/web-apps/plausible.nix
index e5dc1b10360..893dfa10acb 100644
--- a/nixos/modules/services/web-apps/plausible.nix
+++ b/nixos/modules/services/web-apps/plausible.nix
@@ -9,6 +9,8 @@ in {
   options.services.plausible = {
     enable = mkEnableOption (lib.mdDoc "plausible");
 
+    package = mkPackageOptionMD pkgs "plausible" { };
+
     releaseCookiePath = mkOption {
       type = with types; either str path;
       description = lib.mdDoc ''
@@ -180,12 +182,12 @@ in {
 
     services.epmd.enable = true;
 
-    environment.systemPackages = [ pkgs.plausible ];
+    environment.systemPackages = [ cfg.package ];
 
     systemd.services = mkMerge [
       {
         plausible = {
-          inherit (pkgs.plausible.meta) description;
+          inherit (cfg.package.meta) description;
           documentation = [ "https://plausible.io/docs/self-hosting" ];
           wantedBy = [ "multi-user.target" ];
           after = optional cfg.database.clickhouse.setup "clickhouse.service"
@@ -233,7 +235,7 @@ in {
             SMTP_USER_NAME = cfg.mail.smtp.user;
           });
 
-          path = [ pkgs.plausible ]
+          path = [ cfg.package ]
             ++ optional cfg.database.postgres.setup config.services.postgresql.package;
           script = ''
             export CONFIG_DIR=$CREDENTIALS_DIRECTORY
@@ -241,10 +243,10 @@ in {
             export RELEASE_COOKIE="$(< $CREDENTIALS_DIRECTORY/RELEASE_COOKIE )"
 
             # setup
-            ${pkgs.plausible}/createdb.sh
-            ${pkgs.plausible}/migrate.sh
+            ${cfg.package}/createdb.sh
+            ${cfg.package}/migrate.sh
             ${optionalString cfg.adminUser.activate ''
-              if ! ${pkgs.plausible}/init-admin.sh | grep 'already exists'; then
+              if ! ${cfg.package}/init-admin.sh | grep 'already exists'; then
                 psql -d plausible <<< "UPDATE users SET email_verified=true;"
               fi
             ''}
@@ -292,5 +294,5 @@ in {
   };
 
   meta.maintainers = with maintainers; [ ma27 ];
-  meta.doc = ./plausible.xml;
+  meta.doc = ./plausible.md;
 }
diff --git a/nixos/modules/services/web-apps/plausible.xml b/nixos/modules/services/web-apps/plausible.xml
deleted file mode 100644
index 92a571b9fbd..00000000000
--- a/nixos/modules/services/web-apps/plausible.xml
+++ /dev/null
@@ -1,51 +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-plausible">
- <title>Plausible</title>
- <para>
-  <link xlink:href="https://plausible.io/">Plausible</link> is a privacy-friendly alternative to
-  Google analytics.
- </para>
- <section xml:id="module-services-plausible-basic-usage">
-  <title>Basic Usage</title>
-  <para>
-   At first, a secret key is needed to be generated. This can be done with e.g.
-   <screen><prompt>$ </prompt>openssl rand -base64 64</screen>
-  </para>
-  <para>
-   After that, <package>plausible</package> can be deployed like this:
-<programlisting>{
-  services.plausible = {
-    <link linkend="opt-services.plausible.enable">enable</link> = true;
-    adminUser = {
-      <link linkend="opt-services.plausible.adminUser.activate">activate</link> = true; <co xml:id='ex-plausible-cfg-activate' />
-      <link linkend="opt-services.plausible.adminUser.email">email</link> = "admin@localhost";
-      <link linkend="opt-services.plausible.adminUser.passwordFile">passwordFile</link> = "/run/secrets/plausible-admin-pwd";
-    };
-    server = {
-      <link linkend="opt-services.plausible.server.baseUrl">baseUrl</link> = "http://analytics.example.org";
-      <link linkend="opt-services.plausible.server.secretKeybaseFile">secretKeybaseFile</link> = "/run/secrets/plausible-secret-key-base"; <co xml:id='ex-plausible-cfg-secretbase' />
-    };
-  };
-}</programlisting>
-   <calloutlist>
-    <callout arearefs='ex-plausible-cfg-activate'>
-     <para>
-      <varname>activate</varname> is used to skip the email verification of the admin-user that's
-      automatically created by <package>plausible</package>. This is only supported if
-      <package>postgresql</package> is configured by the module. This is done by default, but
-      can be turned off with <xref linkend="opt-services.plausible.database.postgres.setup" />.
-     </para>
-    </callout>
-    <callout arearefs='ex-plausible-cfg-secretbase'>
-     <para>
-      <varname>secretKeybaseFile</varname> is a path to the file which contains the secret generated
-      with <package>openssl</package> as described above.
-     </para>
-    </callout>
-   </calloutlist>
-  </para>
- </section>
-</chapter>
diff --git a/nixos/modules/services/web-apps/snipe-it.nix b/nixos/modules/services/web-apps/snipe-it.nix
index 314a69a73a8..93b0aafab64 100644
--- a/nixos/modules/services/web-apps/snipe-it.nix
+++ b/nixos/modules/services/web-apps/snipe-it.nix
@@ -454,8 +454,9 @@ in {
 
           # A placeholder file for invalid barcodes
           invalid_barcode_location="${cfg.dataDir}/public/uploads/barcodes/invalid_barcode.gif"
-          [ ! -e "$invalid_barcode_location" ] \
-              && cp ${snipe-it}/share/snipe-it/invalid_barcode.gif "$invalid_barcode_location"
+          if [ ! -e "$invalid_barcode_location" ]; then
+              cp ${snipe-it}/share/snipe-it/invalid_barcode.gif "$invalid_barcode_location"
+          fi
         '';
     };
 
diff --git a/nixos/modules/services/web-apps/wiki-js.nix b/nixos/modules/services/web-apps/wiki-js.nix
index b6e5b4594f1..22682002532 100644
--- a/nixos/modules/services/web-apps/wiki-js.nix
+++ b/nixos/modules/services/web-apps/wiki-js.nix
@@ -113,7 +113,13 @@ in {
       documentation = [ "https://docs.requarks.io/" ];
       wantedBy = [ "multi-user.target" ];
 
-      path = with pkgs; [ coreutils ];
+      path = with pkgs; [
+        # Needed for git storage.
+        git
+        # Needed for git+ssh storage.
+        openssh
+      ];
+
       preStart = ''
         ln -sf ${configFile} /var/lib/${cfg.stateDirectoryName}/config.yml
         ln -sf ${pkgs.wiki-js}/server /var/lib/${cfg.stateDirectoryName}
@@ -127,7 +133,7 @@ in {
         WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}";
         DynamicUser = true;
         PrivateTmp = true;
-        ExecStart = "${pkgs.nodejs-16_x}/bin/node ${pkgs.wiki-js}/server";
+        ExecStart = "${pkgs.nodejs_16}/bin/node ${pkgs.wiki-js}/server";
       };
     };
   };
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix
index 43a6d7e75dc..d4c987da114 100644
--- a/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -32,35 +32,59 @@ let
       # Since hard linking directories is not allowed, copying is the next best thing.
 
       # copy additional plugin(s), theme(s) and language(s)
-      ${concatMapStringsSep "\n" (theme: "cp -r ${theme} $out/share/wordpress/wp-content/themes/${theme.name}") cfg.themes}
-      ${concatMapStringsSep "\n" (plugin: "cp -r ${plugin} $out/share/wordpress/wp-content/plugins/${plugin.name}") cfg.plugins}
+      ${concatStringsSep "\n" (mapAttrsToList (name: theme: "cp -r ${theme} $out/share/wordpress/wp-content/themes/${name}") cfg.themes)}
+      ${concatStringsSep "\n" (mapAttrsToList (name: plugin: "cp -r ${plugin} $out/share/wordpress/wp-content/plugins/${name}") cfg.plugins)}
       ${concatMapStringsSep "\n" (language: "cp -r ${language} $out/share/wordpress/wp-content/languages/") cfg.languages}
     '';
   };
 
-  wpConfig = hostName: cfg: pkgs.writeText "wp-config-${hostName}.php" ''
-    <?php
-      define('DB_NAME', '${cfg.database.name}');
-      define('DB_HOST', '${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}');
-      define('DB_USER', '${cfg.database.user}');
-      ${optionalString (cfg.database.passwordFile != null) "define('DB_PASSWORD', file_get_contents('${cfg.database.passwordFile}'));"}
-      define('DB_CHARSET', 'utf8');
-      $table_prefix  = '${cfg.database.tablePrefix}';
-
-      require_once('${stateDir hostName}/secret-keys.php');
-
-      # wordpress is installed onto a read-only file system
-      define('DISALLOW_FILE_EDIT', true);
-      define('AUTOMATIC_UPDATER_DISABLED', true);
-
-      ${cfg.extraConfig}
-
-      if ( !defined('ABSPATH') )
-        define('ABSPATH', dirname(__FILE__) . '/');
+  mergeConfig = cfg: {
+    # wordpress is installed onto a read-only file system
+    DISALLOW_FILE_EDIT = true;
+    AUTOMATIC_UPDATER_DISABLED = true;
+    DB_NAME = cfg.database.name;
+    DB_HOST = "${cfg.database.host}:${if cfg.database.socket != null then cfg.database.socket else toString cfg.database.port}";
+    DB_USER = cfg.database.user;
+    DB_CHARSET = "utf8";
+    # Always set DB_PASSWORD even when passwordFile is not set. This is the
+    # default Wordpress behaviour.
+    DB_PASSWORD =  if (cfg.database.passwordFile != null) then { _file = cfg.database.passwordFile; } else "";
+  } // cfg.settings;
+
+  wpConfig = hostName: cfg: let
+    conf_gen = c: mapAttrsToList (k: v: "define('${k}', ${mkPhpValue v});") cfg.mergedConfig;
+  in pkgs.writeTextFile {
+    name = "wp-config-${hostName}.php";
+    text = ''
+      <?php
+        $table_prefix  = '${cfg.database.tablePrefix}';
+
+        require_once('${stateDir hostName}/secret-keys.php');
+
+        ${cfg.extraConfig}
+        ${concatStringsSep "\n" (conf_gen cfg.mergedConfig)}
+
+        if ( !defined('ABSPATH') )
+          define('ABSPATH', dirname(__FILE__) . '/');
+
+        require_once(ABSPATH . 'wp-settings.php');
+      ?>
+    '';
+    checkPhase = "${pkgs.php81}/bin/php --syntax-check $target";
+  };
 
-      require_once(ABSPATH . 'wp-settings.php');
-    ?>
-  '';
+  mkPhpValue = v: let
+    isHasAttr = s: isAttrs v && hasAttr s v;
+  in
+    if isString v then escapeShellArg v
+    # NOTE: If any value contains a , (comma) this will not get escaped
+    else if isList v && any lib.strings.isCoercibleToString v then escapeShellArg (concatMapStringsSep "," toString v)
+    else if isInt v then toString v
+    else if isBool v then boolToString v
+    else if isHasAttr "_file" then "trim(file_get_contents(${lib.escapeShellArg v._file}))"
+    else if isHasAttr "_raw" then v._raw
+    else abort "The Wordpress config value ${lib.generators.toPretty {} v} can not be encoded."
+  ;
 
   secretsVars = [ "AUTH_KEY" "SECURE_AUTH_KEY" "LOGGED_IN_KEY" "NONCE_KEY" "AUTH_SALT" "SECURE_AUTH_SALT" "LOGGED_IN_SALT" "NONCE_SALT" ];
   secretsScript = hostStateDir: ''
@@ -77,7 +101,7 @@ let
     fi
   '';
 
-  siteOpts = { lib, name, ... }:
+  siteOpts = { lib, name, config, ... }:
     {
       options = {
         package = mkOption {
@@ -106,62 +130,45 @@ let
         };
 
         plugins = mkOption {
-          type = types.listOf types.path;
-          default = [];
+          type = with types; coercedTo
+            (listOf path)
+            (l: warn "setting this option with a list is deprecated"
+              listToAttrs (map (p: nameValuePair (p.name or (throw "${p} does not have a name")) p) l))
+            (attrsOf path);
+          default = {};
           description = lib.mdDoc ''
-            List of path(s) to respective plugin(s) which are copied from the 'plugins' directory.
+            Path(s) to respective plugin(s) which are copied from the 'plugins' directory.
 
             ::: {.note}
             These plugins need to be packaged before use, see example.
             :::
           '';
           example = literalExpression ''
-            let
-              # Wordpress plugin 'embed-pdf-viewer' installation example
-              embedPdfViewerPlugin = pkgs.stdenv.mkDerivation {
-                name = "embed-pdf-viewer-plugin";
-                # Download the theme from the wordpress site
-                src = pkgs.fetchurl {
-                  url = "https://downloads.wordpress.org/plugin/embed-pdf-viewer.2.0.3.zip";
-                  sha256 = "1rhba5h5fjlhy8p05zf0p14c9iagfh96y91r36ni0rmk6y891lyd";
-                };
-                # We need unzip to build this package
-                nativeBuildInputs = [ pkgs.unzip ];
-                # Installing simply means copying all files to the output directory
-                installPhase = "mkdir -p $out; cp -R * $out/";
-              };
-            # And then pass this theme to the themes list like this:
-            in [ embedPdfViewerPlugin ]
+            {
+              inherit (pkgs.wordpressPackages.plugins) embed-pdf-viewer-plugin;
+            }
           '';
         };
 
         themes = mkOption {
-          type = types.listOf types.path;
-          default = [];
+          type = with types; coercedTo
+            (listOf path)
+            (l: warn "setting this option with a list is deprecated"
+              listToAttrs (map (p: nameValuePair (p.name or (throw "${p} does not have a name")) p) l))
+            (attrsOf path);
+          default = { inherit (pkgs.wordpressPackages.themes) twentytwentythree; };
+          defaultText = literalExpression "{ inherit (pkgs.wordpressPackages.themes) twentytwentythree; }";
           description = lib.mdDoc ''
-            List of path(s) to respective theme(s) which are copied from the 'theme' directory.
+            Path(s) to respective theme(s) which are copied from the 'theme' directory.
 
             ::: {.note}
             These themes need to be packaged before use, see example.
             :::
           '';
           example = literalExpression ''
-            let
-              # Let's package the responsive theme
-              responsiveTheme = pkgs.stdenv.mkDerivation {
-                name = "responsive-theme";
-                # Download the theme from the wordpress site
-                src = pkgs.fetchurl {
-                  url = "https://downloads.wordpress.org/theme/responsive.3.14.zip";
-                  sha256 = "0rjwm811f4aa4q43r77zxlpklyb85q08f9c8ns2akcarrvj5ydx3";
-                };
-                # We need unzip to build this package
-                nativeBuildInputs = [ pkgs.unzip ];
-                # Installing simply means copying all files to the output directory
-                installPhase = "mkdir -p $out; cp -R * $out/";
-              };
-            # And then pass this theme to the themes list like this:
-            in [ responsiveTheme ]
+            {
+              inherit (pkgs.wordpressPackages.themes) responsive-theme;
+            }
           '';
         };
 
@@ -283,6 +290,42 @@ let
           '';
         };
 
+        settings = mkOption {
+          type = types.attrsOf types.anything;
+          default = {};
+          description = lib.mdDoc ''
+            Structural Wordpress configuration.
+            Refer to <https://developer.wordpress.org/apis/wp-config-php>
+            for details and supported values.
+          '';
+          example = literalExpression ''
+            {
+              WP_DEFAULT_THEME = "twentytwentytwo";
+              WP_SITEURL = "https://example.org";
+              WP_HOME = "https://example.org";
+              WP_DEBUG = true;
+              WP_DEBUG_DISPLAY = true;
+              WPLANG = "de_DE";
+              FORCE_SSL_ADMIN = true;
+              AUTOMATIC_UPDATER_DISABLED = true;
+            }
+          '';
+        };
+
+        mergedConfig = mkOption {
+          readOnly = true;
+          default = mergeConfig config;
+          defaultText = literalExpression ''
+            {
+              DISALLOW_FILE_EDIT = true;
+              AUTOMATIC_UPDATER_DISABLED = true;
+            }
+          '';
+          description = lib.mdDoc ''
+            Read only representation of the final configuration.
+          '';
+        };
+
         extraConfig = mkOption {
           type = types.lines;
           default = "";
@@ -290,11 +333,16 @@ let
             Any additional text to be appended to the wp-config.php
             configuration file. This is a PHP script. For configuration
             settings, see <https://codex.wordpress.org/Editing_wp-config.php>.
+
+            **Note**: Please pass structured settings via
+            `services.wordpress.sites.${name}.settings` instead.
           '';
           example = ''
-            define( 'AUTOSAVE_INTERVAL', 60 ); // Seconds
+            @ini_set( 'log_errors', 'Off' );
+            @ini_set( 'display_errors', 'On' );
           '';
         };
+
       };
 
       config.virtualHost.hostName = mkDefault name;
diff --git a/nixos/modules/services/web-apps/writefreely.nix b/nixos/modules/services/web-apps/writefreely.nix
index dec00b46f33..a7671aa717f 100644
--- a/nixos/modules/services/web-apps/writefreely.nix
+++ b/nixos/modules/services/web-apps/writefreely.nix
@@ -10,12 +10,11 @@ let
   format = pkgs.formats.ini {
     mkKeyValue = key: value:
       let
-        value' = if builtins.isNull value then
-          ""
-        else if builtins.isBool value then
-          if value == true then "true" else "false"
-        else
-          toString value;
+        value' = lib.optionalString (value != null)
+          (if builtins.isBool value then
+            if value == true then "true" else "false"
+          else
+            toString value);
       in "${key} = ${value'}";
   };
 
diff --git a/nixos/modules/services/web-servers/caddy/default.nix b/nixos/modules/services/web-servers/caddy/default.nix
index 50213ec252f..f5a9cfac5d7 100644
--- a/nixos/modules/services/web-servers/caddy/default.nix
+++ b/nixos/modules/services/web-servers/caddy/default.nix
@@ -35,7 +35,8 @@ let
 
       Caddyfile-formatted = pkgs.runCommand "Caddyfile-formatted" { nativeBuildInputs = [ cfg.package ]; } ''
         mkdir -p $out
-        ${cfg.package}/bin/caddy fmt ${Caddyfile}/Caddyfile > $out/Caddyfile
+        cp --no-preserve=mode ${Caddyfile}/Caddyfile $out/Caddyfile
+        caddy fmt --overwrite $out/Caddyfile
       '';
     in
       "${if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform then Caddyfile-formatted else Caddyfile}/Caddyfile";
diff --git a/nixos/modules/services/web-servers/fcgiwrap.nix b/nixos/modules/services/web-servers/fcgiwrap.nix
index f9c91fb35db..3a57ef38306 100644
--- a/nixos/modules/services/web-servers/fcgiwrap.nix
+++ b/nixos/modules/services/web-servers/fcgiwrap.nix
@@ -54,7 +54,7 @@ in {
 
       serviceConfig = {
         ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${builtins.toString cfg.preforkProcesses} ${
-          if (cfg.socketType != "unix") then "-s ${cfg.socketType}:${cfg.socketAddress}" else ""
+          optionalString (cfg.socketType != "unix") "-s ${cfg.socketType}:${cfg.socketAddress}"
         }";
       } // (if cfg.user != null && cfg.group != null then {
         User = cfg.user;
diff --git a/nixos/modules/services/web-servers/garage.md b/nixos/modules/services/web-servers/garage.md
new file mode 100644
index 00000000000..b1003e5dae1
--- /dev/null
+++ b/nixos/modules/services/web-servers/garage.md
@@ -0,0 +1,96 @@
+# Garage {#module-services-garage}
+
+[Garage](https://garagehq.deuxfleurs.fr/)
+is an open-source, self-hostable S3 store, simpler than MinIO, for geodistributed stores.
+The server setup can be automated using
+[services.garage](#opt-services.garage.enable). A
+ client configured to your local Garage instance is available in
+ the global environment as `garage-manage`.
+
+The current default by NixOS is `garage_0_8` which is also the latest
+major version available.
+
+## General considerations on upgrades {#module-services-garage-upgrade-scenarios}
+
+Garage provides a cookbook documentation on how to upgrade:
+<https://garagehq.deuxfleurs.fr/documentation/cookbook/upgrading/>
+
+::: {.warning}
+Garage has two types of upgrades: patch-level upgrades and minor/major version upgrades.
+
+In all cases, you should read the changelog and ideally test the upgrade on a staging cluster.
+
+Checking the health of your cluster can be achieved using `garage-manage repair`.
+:::
+
+::: {.warning}
+Until 1.0 is released, patch-level upgrades are considered as minor version upgrades.
+Minor version upgrades are considered as major version upgrades.
+i.e. 0.6 to 0.7 is a major version upgrade.
+:::
+
+  - **Straightforward upgrades (patch-level upgrades).**
+    Upgrades must be performed one by one, i.e. for each node, stop it, upgrade it : change [stateVersion](#opt-system.stateVersion) or [services.garage.package](#opt-services.garage.package), restart it if it was not already by switching.
+  - **Multiple version upgrades.**
+    Garage do not provide any guarantee on moving more than one major-version forward.
+    E.g., if you're on `0.7`, you cannot upgrade to `0.9`.
+    You need to upgrade to `0.8` first.
+    As long as [stateVersion](#opt-system.stateVersion) is declared properly,
+    this is enforced automatically. The module will issue a warning to remind the user to upgrade to latest
+    Garage *after* that deploy.
+
+## Advanced upgrades (minor/major version upgrades) {#module-services-garage-advanced-upgrades}
+
+Here are some baseline instructions to handle advanced upgrades in Garage, when in doubt, please refer to upstream instructions.
+
+  - Disable API and web access to Garage.
+  - Perform `garage-manage repair --all-nodes --yes tables` and `garage-manage repair --all-nodes --yes blocks`.
+  - Verify the resulting logs and check that data is synced properly between all nodes.
+    If you have time, do additional checks (`scrub`, `block_refs`, etc.).
+  - Check if queues are empty by `garage-manage stats` or through monitoring tools.
+  - Run `systemctl stop garage` to stop the actual Garage version.
+  - Backup the metadata folder of ALL your nodes, e.g. for a metadata directory (the default one) in `/var/lib/garage/meta`,
+    you can run `pushd /var/lib/garage; tar -acf meta-v0.7.tar.zst meta/; popd`.
+  - Run the offline migration: `nix-shell -p garage_0_8 --run "garage offline-repair --yes"`, this can take some time depending on how many objects are stored in your cluster.
+  - Bump Garage version in your NixOS configuration, either by changing [stateVersion](#opt-system.stateVersion) or bumping [services.garage.package](#opt-services.garage.package), this should restart Garage automatically.
+  - Perform `garage-manage repair --all-nodes --yes tables` and `garage-manage repair --all-nodes --yes blocks`.
+  - Wait for a full table sync to run.
+
+Your upgraded cluster should be in a working state, re-enable API and web access.
+
+## Maintainer information {#module-services-garage-maintainer-info}
+
+As stated in the previous paragraph, we must provide a clean upgrade-path for Garage
+since it cannot move more than one major version forward on a single upgrade. This chapter
+adds some notes how Garage updates should be rolled out in the future.
+This is inspired from how Nextcloud does it.
+
+While patch-level updates are no problem and can be done directly in the
+package-expression (and should be backported to supported stable branches after that),
+major-releases should be added in a new attribute (e.g. Garage `v0.8.0`
+should be available in `nixpkgs` as `pkgs.garage_0_8_0`).
+To provide simple upgrade paths it's generally useful to backport those as well to stable
+branches. As long as the package-default isn't altered, this won't break existing setups.
+After that, the versioning-warning in the `garage`-module should be
+updated to make sure that the
+[package](#opt-services.garage.package)-option selects the latest version
+on fresh setups.
+
+If major-releases will be abandoned by upstream, we should check first if those are needed
+in NixOS for a safe upgrade-path before removing those. In that case we shold keep those
+packages, but mark them as insecure in an expression like this (in
+`<nixpkgs/pkgs/tools/filesystem/garage/default.nix>`):
+```
+/* ... */
+{
+  garage_0_7_3 = generic {
+    version = "0.7.3";
+    sha256 = "0000000000000000000000000000000000000000000000000000";
+    eol = true;
+  };
+}
+```
+
+Ideally we should make sure that it's possible to jump two NixOS versions forward:
+i.e. the warnings and the logic in the module should guard a user to upgrade from a
+Garage on e.g. 22.11 to a Garage on 23.11.
diff --git a/nixos/modules/services/web-servers/garage.nix b/nixos/modules/services/web-servers/garage.nix
index 76ab273483e..ebd41473939 100644
--- a/nixos/modules/services/web-servers/garage.nix
+++ b/nixos/modules/services/web-servers/garage.nix
@@ -8,7 +8,10 @@ let
   configFile = toml.generate "garage.toml" cfg.settings;
 in
 {
-  meta.maintainers = [ maintainers.raitobezarius ];
+  meta = {
+    doc = ./garage.md;
+    maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+  };
 
   options.services.garage = {
     enable = mkEnableOption (lib.mdDoc "Garage Object Storage (S3 compatible)");
@@ -48,18 +51,20 @@ in
             default = "none";
             type = types.enum ([ "none" "1" "2" "3" 1 2 3 ]);
             apply = v: toString v;
-            description = lib.mdDoc "Garage replication mode, defaults to none, see: <https://garagehq.deuxfleurs.fr/reference_manual/configuration.html#replication_mode> for reference.";
+            description = lib.mdDoc "Garage replication mode, defaults to none, see: <https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/#replication-mode> for reference.";
           };
         };
       };
-      description = lib.mdDoc "Garage configuration, see <https://garagehq.deuxfleurs.fr/reference_manual/configuration.html> for reference.";
+      description = lib.mdDoc "Garage configuration, see <https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/> for reference.";
     };
 
     package = mkOption {
-      default = pkgs.garage;
-      defaultText = literalExpression "pkgs.garage";
+      # TODO: when 23.05 is released and if Garage 0.9 is the default, put a stateVersion check.
+      default = if versionAtLeast config.system.stateVersion "23.05" then pkgs.garage_0_8
+                else pkgs.garage_0_7;
+      defaultText = literalExpression "pkgs.garage_0_7";
       type = types.package;
-      description = lib.mdDoc "Garage package to use.";
+      description = lib.mdDoc "Garage package to use, if you are upgrading from a major version, please read NixOS and Garage release notes for upgrade instructions.";
     };
   };
 
diff --git a/nixos/modules/services/web-servers/jboss/builder.sh b/nixos/modules/services/web-servers/jboss/builder.sh
index 0e5af324c13..ac573089cd5 100644
--- a/nixos/modules/services/web-servers/jboss/builder.sh
+++ b/nixos/modules/services/web-servers/jboss/builder.sh
@@ -1,5 +1,6 @@
 set -e
 
+if [ -e .attrs.sh ]; then source .attrs.sh; fi
 source $stdenv/setup
 
 mkdir -p $out/bin
diff --git a/nixos/modules/services/web-servers/lighttpd/default.nix b/nixos/modules/services/web-servers/lighttpd/default.nix
index 811afe8e0af..0438e12e7da 100644
--- a/nixos/modules/services/web-servers/lighttpd/default.nix
+++ b/nixos/modules/services/web-servers/lighttpd/default.nix
@@ -64,7 +64,7 @@ let
   ];
 
   maybeModuleString = moduleName:
-    if elem moduleName cfg.enableModules then ''"${moduleName}"'' else "";
+    optionalString (elem moduleName cfg.enableModules) ''"${moduleName}"'';
 
   modulesIncludeString = concatStringsSep ",\n"
     (filter (x: x != "") (map maybeModuleString allKnownModules));
@@ -106,15 +106,15 @@ let
       static-file.exclude-extensions = ( ".fcgi", ".php", ".rb", "~", ".inc" )
       index-file.names = ( "index.html" )
 
-      ${if cfg.mod_userdir then ''
+      ${optionalString cfg.mod_userdir ''
         userdir.path = "public_html"
-      '' else ""}
+      ''}
 
-      ${if cfg.mod_status then ''
+      ${optionalString cfg.mod_status ''
         status.status-url = "/server-status"
         status.statistics-url = "/server-statistics"
         status.config-url = "/server-config"
-      '' else ""}
+      ''}
 
       ${cfg.extraConfig}
     '';
diff --git a/nixos/modules/services/web-servers/minio.nix b/nixos/modules/services/web-servers/minio.nix
index 1a9eacb431b..21bec4f63a8 100644
--- a/nixos/modules/services/web-servers/minio.nix
+++ b/nixos/modules/services/web-servers/minio.nix
@@ -60,7 +60,7 @@ in
       '';
     };
 
-    rootCredentialsFile = mkOption  {
+    rootCredentialsFile = mkOption {
       type = types.nullOr types.path;
       default = null;
       description = lib.mdDoc ''
@@ -96,29 +96,62 @@ in
   config = mkIf cfg.enable {
     warnings = optional ((cfg.accessKey != "") || (cfg.secretKey != "")) "services.minio.`accessKey` and services.minio.`secretKey` are deprecated, please use services.minio.`rootCredentialsFile` instead.";
 
-    systemd.tmpfiles.rules = [
-      "d '${cfg.configDir}' - minio minio - -"
-    ] ++ (map (x:  "d '" + x + "' - minio minio - - ") cfg.dataDir);
-
-    systemd.services.minio = {
-      description = "Minio Object Storage";
-      after = [ "network-online.target" ];
-      wantedBy = [ "multi-user.target" ];
-      serviceConfig = {
-        ExecStart = "${cfg.package}/bin/minio server --json --address ${cfg.listenAddress} --console-address ${cfg.consoleAddress} --config-dir=${cfg.configDir} ${toString cfg.dataDir}";
-        Type = "simple";
-        User = "minio";
-        Group = "minio";
-        LimitNOFILE = 65536;
-        EnvironmentFile = if (cfg.rootCredentialsFile != null) then cfg.rootCredentialsFile
-                          else if ((cfg.accessKey != "") || (cfg.secretKey != "")) then (legacyCredentials cfg)
-                          else null;
+    systemd = lib.mkMerge [{
+      tmpfiles.rules = [
+        "d '${cfg.configDir}' - minio minio - -"
+      ] ++ (map (x: "d '" + x + "' - minio minio - - ") cfg.dataDir);
+
+      services.minio = {
+        description = "Minio Object Storage";
+        after = [ "network-online.target" ];
+        wantedBy = [ "multi-user.target" ];
+        serviceConfig = {
+          ExecStart = "${cfg.package}/bin/minio server --json --address ${cfg.listenAddress} --console-address ${cfg.consoleAddress} --config-dir=${cfg.configDir} ${toString cfg.dataDir}";
+          Type = "simple";
+          User = "minio";
+          Group = "minio";
+          LimitNOFILE = 65536;
+          EnvironmentFile =
+            if (cfg.rootCredentialsFile != null) then cfg.rootCredentialsFile
+            else if ((cfg.accessKey != "") || (cfg.secretKey != "")) then (legacyCredentials cfg)
+            else null;
+        };
+        environment = {
+          MINIO_REGION = "${cfg.region}";
+          MINIO_BROWSER = "${if cfg.browser then "on" else "off"}";
+        };
       };
-      environment = {
-        MINIO_REGION = "${cfg.region}";
-        MINIO_BROWSER = "${if cfg.browser then "on" else "off"}";
-      };
-    };
+    }
+
+      (lib.mkIf (cfg.rootCredentialsFile != null) {
+        # The service will fail if the credentials file is missing
+        services.minio.unitConfig.ConditionPathExists = cfg.rootCredentialsFile;
+
+        # The service will not restart if the credentials file has
+        # been changed. This can cause stale root credentials.
+        paths.minio-root-credentials = {
+          wantedBy = [ "multi-user.target" ];
+
+          pathConfig = {
+            PathChanged = [ cfg.rootCredentialsFile ];
+            Unit = "minio-restart.service";
+          };
+        };
+
+        services.minio-restart = {
+          description = "Restart MinIO";
+
+          script = ''
+            systemctl restart minio.service
+          '';
+
+          serviceConfig = {
+            Type = "oneshot";
+            Restart = "on-failure";
+            RestartSec = 5;
+          };
+        };
+      })];
 
     users.users.minio = {
       group = "minio";
diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix
index 953f3163293..f5fa2ef5f86 100644
--- a/nixos/modules/services/web-servers/nginx/default.nix
+++ b/nixos/modules/services/web-servers/nginx/default.nix
@@ -4,7 +4,7 @@ with lib;
 
 let
   cfg = config.services.nginx;
-  certs = config.security.acme.certs;
+  inherit (config.security.acme) certs;
   vhostsConfigs = mapAttrsToList (vhostName: vhostConfig: vhostConfig) virtualHosts;
   acmeEnabledVhosts = filter (vhostConfig: vhostConfig.enableACME || vhostConfig.useACMEHost != null) vhostsConfigs;
   dependentCertNames = unique (map (hostOpts: hostOpts.certName) acmeEnabledVhosts);
@@ -27,7 +27,44 @@ let
                               else "${certs.${certName}.directory}/chain.pem";
     })
   ) cfg.virtualHosts;
-  enableIPv6 = config.networking.enableIPv6;
+  inherit (config.networking) enableIPv6;
+
+  # Mime.types values are taken from brotli sample configuration - https://github.com/google/ngx_brotli
+  # and Nginx Server Configs - https://github.com/h5bp/server-configs-nginx
+  # "text/html" is implicitly included in {brotli,gzip,zstd}_types
+  compressMimeTypes = [
+    "application/atom+xml"
+    "application/geo+json"
+    "application/json"
+    "application/ld+json"
+    "application/manifest+json"
+    "application/rdf+xml"
+    "application/vnd.ms-fontobject"
+    "application/wasm"
+    "application/x-rss+xml"
+    "application/x-web-app-manifest+json"
+    "application/xhtml+xml"
+    "application/xliff+xml"
+    "application/xml"
+    "font/collection"
+    "font/otf"
+    "font/ttf"
+    "image/bmp"
+    "image/svg+xml"
+    "image/vnd.microsoft.icon"
+    "text/cache-manifest"
+    "text/calendar"
+    "text/css"
+    "text/csv"
+    "text/javascript"
+    "text/markdown"
+    "text/plain"
+    "text/vcard"
+    "text/vnd.rim.location.xloc"
+    "text/vtt"
+    "text/x-component"
+    "text/xml"
+  ];
 
   defaultFastcgiParams = {
     SCRIPT_FILENAME   = "$document_root$fastcgi_script_name";
@@ -65,22 +102,36 @@ let
     proxy_set_header        X-Forwarded-Server $host;
   '';
 
+  proxyCachePathConfig = concatStringsSep "\n" (mapAttrsToList (name: proxyCachePath: ''
+    proxy_cache_path ${concatStringsSep " " [
+      "/var/cache/nginx/${name}"
+      "keys_zone=${proxyCachePath.keysZoneName}:${proxyCachePath.keysZoneSize}"
+      "levels=${proxyCachePath.levels}"
+      "use_temp_path=${if proxyCachePath.useTempPath then "on" else "off"}"
+      "inactive=${proxyCachePath.inactive}"
+      "max_size=${proxyCachePath.maxSize}"
+    ]};
+  '') (filterAttrs (name: conf: conf.enable) cfg.proxyCachePath));
+
+  toUpstreamParameter = key: value:
+    if builtins.isBool value
+    then lib.optionalString value key
+    else "${key}=${toString value}";
+
   upstreamConfig = toString (flip mapAttrsToList cfg.upstreams (name: upstream: ''
     upstream ${name} {
       ${toString (flip mapAttrsToList upstream.servers (name: server: ''
-        server ${name} ${optionalString server.backup "backup"};
+        server ${name} ${concatStringsSep " " (mapAttrsToList toUpstreamParameter server)};
       ''))}
       ${upstream.extraConfig}
     }
   ''));
 
   commonHttpConfig = ''
-      # The mime type definitions included with nginx are very incomplete, so
-      # we use a list of mime types from the mailcap package, which is also
-      # used by most other Linux distributions by default.
-      include ${pkgs.mailcap}/etc/nginx/mime.types;
+      # Load mime types.
+      include ${cfg.defaultMimeTypes};
       # When recommendedOptimisation is disabled nginx fails to start because the mailmap mime.types database
-      # contains 1026 enries and the default is only 1024. Setting to a higher number to remove the need to
+      # contains 1026 entries and the default is only 1024. Setting to a higher number to remove the need to
       # overwrite it because nginx does not allow duplicated settings.
       types_hash_max_size 4096;
 
@@ -112,7 +163,7 @@ let
       ''}
       ${upstreamConfig}
 
-      ${optionalString (cfg.recommendedOptimisation) ''
+      ${optionalString cfg.recommendedOptimisation ''
         # optimisation
         sendfile on;
         tcp_nopush on;
@@ -124,7 +175,7 @@ let
       ${optionalString (cfg.sslCiphers != null) "ssl_ciphers ${cfg.sslCiphers};"}
       ${optionalString (cfg.sslDhparam != null) "ssl_dhparam ${cfg.sslDhparam};"}
 
-      ${optionalString (cfg.recommendedTlsSettings) ''
+      ${optionalString cfg.recommendedTlsSettings ''
         # Keep in sync with https://ssl-config.mozilla.org/#server=nginx&config=intermediate
 
         ssl_session_timeout 1d;
@@ -140,30 +191,44 @@ let
         ssl_stapling_verify on;
       ''}
 
-      ${optionalString (cfg.recommendedGzipSettings) ''
+      ${optionalString cfg.recommendedBrotliSettings ''
+        brotli on;
+        brotli_static on;
+        brotli_comp_level 5;
+        brotli_window 512k;
+        brotli_min_length 256;
+        brotli_types ${lib.concatStringsSep " " compressMimeTypes};
+      ''}
+
+      ${optionalString cfg.recommendedGzipSettings
+        # https://docs.nginx.com/nginx/admin-guide/web-server/compression/
+      ''
         gzip on;
-        gzip_proxied any;
-        gzip_comp_level 5;
-        gzip_types
-          application/atom+xml
-          application/javascript
-          application/json
-          application/xml
-          application/xml+rss
-          image/svg+xml
-          text/css
-          text/javascript
-          text/plain
-          text/xml;
+        gzip_static on;
         gzip_vary on;
+        gzip_comp_level 5;
+        gzip_min_length 256;
+        gzip_proxied expired no-cache no-store private auth;
+        gzip_types ${lib.concatStringsSep " " compressMimeTypes};
       ''}
 
-      ${optionalString (cfg.recommendedProxySettings) ''
+      ${optionalString cfg.recommendedZstdSettings ''
+        zstd on;
+        zstd_comp_level 9;
+        zstd_min_length 256;
+        zstd_static on;
+        zstd_types ${lib.concatStringsSep " " compressMimeTypes};
+      ''}
+
+      ${optionalString cfg.recommendedProxySettings ''
         proxy_redirect          off;
         proxy_connect_timeout   ${cfg.proxyTimeout};
         proxy_send_timeout      ${cfg.proxyTimeout};
         proxy_read_timeout      ${cfg.proxyTimeout};
         proxy_http_version      1.1;
+        # don't let clients close the keep-alive connection to upstream. See the nginx blog for details:
+        # https://www.nginx.com/blog/avoiding-top-10-nginx-configuration-mistakes/#no-keepalives
+        proxy_set_header        "Connection" "";
         include ${recommendedProxyConfig};
       ''}
 
@@ -192,17 +257,9 @@ let
 
       server_tokens ${if cfg.serverTokens then "on" else "off"};
 
-      ${optionalString (cfg.proxyCache.enable) ''
-        proxy_cache_path /var/cache/nginx keys_zone=${cfg.proxyCache.keysZoneName}:${cfg.proxyCache.keysZoneSize}
-                                          levels=${cfg.proxyCache.levels}
-                                          use_temp_path=${if cfg.proxyCache.useTempPath then "on" else "off"}
-                                          inactive=${cfg.proxyCache.inactive}
-                                          max_size=${cfg.proxyCache.maxSize};
-      ''}
-
       ${cfg.commonHttpConfig}
 
-      ${vhosts}
+      ${proxyCachePathConfig}
 
       ${optionalString cfg.statusPage ''
         server {
@@ -221,6 +278,8 @@ let
         }
       ''}
 
+      ${vhosts}
+
       ${cfg.appendHttpConfig}
     }''}
 
@@ -241,7 +300,7 @@ let
 
   configPath = if cfg.enableReload
     then "/etc/nginx/nginx.conf"
-    else finalConfigFile;
+    else configFile;
 
   execCommand = "${cfg.package}/bin/nginx -c '${configPath}'";
 
@@ -263,13 +322,16 @@ let
             else defaultListen;
 
         listenString = { addr, port, ssl, extraParameters ? [], ... }:
-          (if ssl && vhost.http3 then "
-          # UDP listener for **QUIC+HTTP/3
-          listen ${addr}:${toString port} http3 "
+          # UDP listener for QUIC transport protocol.
+          (optionalString (ssl && vhost.quic) ("
+            listen ${addr}:${toString port} quic "
           + optionalString vhost.default "default_server "
           + optionalString vhost.reuseport "reuseport "
-          + optionalString (extraParameters != []) (concatStringsSep " " extraParameters)
-          + ";" else "")
+          + optionalString (extraParameters != []) (concatStringsSep " " (
+            let inCompatibleParameters = [ "ssl" "proxy_protocol" "http2" ];
+                isCompatibleParameter = param: !(any (p: p == param) inCompatibleParameters);
+            in filter isCompatibleParameter extraParameters))
+          + ";"))
           + "
 
             listen ${addr}:${toString port} "
@@ -315,6 +377,10 @@ let
         server {
           ${concatMapStringsSep "\n" listenString hostListen}
           server_name ${vhost.serverName} ${concatStringsSep " " vhost.serverAliases};
+          ${optionalString (hasSSL && vhost.quic) ''
+            http3 ${if vhost.http3 then "on" else "off"};
+            http3_hq ${if vhost.http3_hq then "on" else "off"};
+          ''}
           ${acmeLocation}
           ${optionalString (vhost.root != null) "root ${vhost.root};"}
           ${optionalString (vhost.globalRedirect != null) ''
@@ -336,9 +402,10 @@ let
             ssl_conf_command Options KTLS;
           ''}
 
-          ${optionalString (hasSSL && vhost.http3) ''
+          ${optionalString (hasSSL && vhost.quic && vhost.http3)
             # Advertise that HTTP/3 is available
-            add_header Alt-Svc 'h3=":443"; ma=86400' always;
+          ''
+            add_header Alt-Svc 'h3=":$server_port"; ma=86400';
           ''}
 
           ${mkBasicAuth vhostName vhost}
@@ -393,38 +460,6 @@ let
   );
 
   mkCertOwnershipAssertion = import ../../../security/acme/mk-cert-ownership-assertion.nix;
-
-  snakeOilCert = pkgs.runCommand "nginx-config-validate-cert" { nativeBuildInputs = [ pkgs.openssl.bin ]; } ''
-    mkdir $out
-    openssl genrsa -des3 -passout pass:xxxxx -out server.pass.key 2048
-    openssl rsa -passin pass:xxxxx -in server.pass.key -out $out/server.key
-    openssl req -new -key $out/server.key -out server.csr \
-    -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
-    openssl x509 -req -days 1 -in server.csr -signkey $out/server.key -out $out/server.crt
-  '';
-  validatedConfigFile = pkgs.runCommand "validated-nginx.conf" { nativeBuildInputs = [ cfg.package ]; } ''
-    # nginx absolutely wants to read the certificates even when told to only validate config, so let's provide fake certs
-    sed ${configFile} \
-    -e "s|ssl_certificate .*;|ssl_certificate ${snakeOilCert}/server.crt;|g" \
-    -e "s|ssl_trusted_certificate .*;|ssl_trusted_certificate ${snakeOilCert}/server.crt;|g" \
-    -e "s|ssl_certificate_key .*;|ssl_certificate_key ${snakeOilCert}/server.key;|g" \
-    > conf
-
-    LD_PRELOAD=${pkgs.libredirect}/lib/libredirect.so \
-      NIX_REDIRECTS="/etc/resolv.conf=/dev/null" \
-      nginx -t -c $(readlink -f ./conf) > out 2>&1 || true
-    if ! grep -q "syntax is ok" out; then
-      echo nginx config validation failed.
-      echo config was ${configFile}.
-      echo 'in case of false positive, set `services.nginx.validateConfig` to false.'
-      echo nginx output:
-      cat out
-      exit 1
-    fi
-    cp ${configFile} $out
-  '';
-
-  finalConfigFile = if cfg.validateConfig then validatedConfigFile else configFile;
 in
 
 {
@@ -456,11 +491,34 @@ in
         '';
       };
 
+      recommendedBrotliSettings = mkOption {
+        default = false;
+        type = types.bool;
+        description = lib.mdDoc ''
+          Enable recommended brotli settings.
+          Learn more about compression in Brotli format [here](https://github.com/google/ngx_brotli/).
+
+          This adds `pkgs.nginxModules.brotli` to `services.nginx.additionalModules`.
+        '';
+      };
+
       recommendedGzipSettings = mkOption {
         default = false;
         type = types.bool;
         description = lib.mdDoc ''
           Enable recommended gzip settings.
+          Learn more about compression in Gzip format [here](https://docs.nginx.com/nginx/admin-guide/web-server/compression/).
+        '';
+      };
+
+      recommendedZstdSettings = mkOption {
+        default = false;
+        type = types.bool;
+        description = lib.mdDoc ''
+          Enable recommended zstd settings.
+          Learn more about compression in Zstd format [here](https://github.com/tokers/zstd-nginx-module).
+
+          This adds `pkgs.nginxModules.zstd` to `services.nginx.additionalModules`.
         '';
       };
 
@@ -509,12 +567,24 @@ in
         '';
       };
 
+      defaultMimeTypes = mkOption {
+        type = types.path;
+        default = "${pkgs.mailcap}/etc/nginx/mime.types";
+        defaultText = literalExpression "$''{pkgs.mailcap}/etc/nginx/mime.types";
+        example = literalExpression "$''{pkgs.nginx}/conf/mime.types";
+        description = lib.mdDoc ''
+          Default MIME types for NGINX, as MIME types definitions from NGINX are very incomplete,
+          we use by default the ones bundled in the mailcap package, used by most of the other
+          Linux distributions.
+        '';
+      };
+
       package = mkOption {
         default = pkgs.nginxStable;
         defaultText = literalExpression "pkgs.nginxStable";
         type = types.package;
         apply = p: p.override {
-          modules = p.modules ++ cfg.additionalModules;
+          modules = lib.unique (p.modules ++ cfg.additionalModules);
         };
         description = lib.mdDoc ''
           Nginx package to use. This defaults to the stable version. Note
@@ -523,23 +593,13 @@ in
         '';
       };
 
-      validateConfig = mkOption {
-        default = pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform;
-        defaultText = literalExpression "pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform";
-        type = types.bool;
-        description = lib.mdDoc ''
-          Validate the generated nginx config at build time. The check is not very robust and can be disabled in case of false positives. This is notably the case when cross-compiling or when using `include` with files outside of the store.
-        '';
-      };
-
       additionalModules = mkOption {
         default = [];
         type = types.listOf (types.attrsOf types.anything);
-        example = literalExpression "[ pkgs.nginxModules.brotli ]";
+        example = literalExpression "[ pkgs.nginxModules.echo ]";
         description = lib.mdDoc ''
           Additional [third-party nginx modules](https://www.nginx.com/resources/wiki/modules/)
-          to install. Packaged modules are available in
-          `pkgs.nginxModules`.
+          to install. Packaged modules are available in `pkgs.nginxModules`.
         '';
       };
 
@@ -758,10 +818,10 @@ in
           '';
       };
 
-      proxyCache = mkOption {
-        type = types.submodule {
+      proxyCachePath = mkOption {
+        type = types.attrsOf (types.submodule ({ ... }: {
           options = {
-            enable = mkEnableOption (lib.mdDoc "Enable proxy cache");
+            enable = mkEnableOption (lib.mdDoc "this proxy cache path entry");
 
             keysZoneName = mkOption {
               type = types.str;
@@ -819,9 +879,12 @@ in
               description = lib.mdDoc "Set maximum cache size";
             };
           };
-        };
+        }));
         default = {};
-        description = lib.mdDoc "Configure proxy cache";
+        description = lib.mdDoc ''
+          Configure a proxy cache path entry.
+          See <http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_path> for documentation.
+        '';
       };
 
       resolver = mkOption {
@@ -864,6 +927,7 @@ in
           options = {
             servers = mkOption {
               type = types.attrsOf (types.submodule {
+                freeformType = types.attrsOf (types.oneOf [ types.bool types.int types.str ]);
                 options = {
                   backup = mkOption {
                     type = types.bool;
@@ -877,9 +941,11 @@ in
               });
               description = lib.mdDoc ''
                 Defines the address and other parameters of the upstream servers.
+                See [the documentation](https://nginx.org/en/docs/http/ngx_http_upstream_module.html#server)
+                for the available parameters.
               '';
               default = {};
-              example = { "127.0.0.1:8000" = {}; };
+              example = lib.literalMD "see [](#opt-services.nginx.upstreams)";
             };
             extraConfig = mkOption {
               type = types.lines;
@@ -894,14 +960,23 @@ in
           Defines a group of servers to use as proxy target.
         '';
         default = {};
-        example = literalExpression ''
-          "backend_server" = {
-            servers = { "127.0.0.1:8000" = {}; };
-            extraConfig = ''''
+        example = {
+          "backend" = {
+            servers = {
+              "backend1.example.com:8080" = { weight = 5; };
+              "backend2.example.com" = { max_fails = 3; fail_timeout = "30s"; };
+              "backend3.example.com" = {};
+              "backup1.example.com" = { backup = true; };
+              "backup2.example.com" = { backup = true; };
+            };
+            extraConfig = ''
               keepalive 16;
-            '''';
+            '';
           };
-        '';
+          "memcached" = {
+            servers."unix:/run//memcached/memcached.sock" = {};
+          };
+        };
       };
 
       virtualHosts = mkOption {
@@ -932,11 +1007,15 @@ in
       The Nginx log directory has been moved to /var/log/nginx, the cache directory
       to /var/cache/nginx. The option services.nginx.stateDir has been removed.
     '')
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "inactive" ] [ "services" "nginx" "proxyCachePath" "" "inactive" ])
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "useTempPath" ] [ "services" "nginx" "proxyCachePath" "" "useTempPath" ])
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "levels" ] [ "services" "nginx" "proxyCachePath" "" "levels" ])
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "keysZoneSize" ] [ "services" "nginx" "proxyCachePath" "" "keysZoneSize" ])
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "keysZoneName" ] [ "services" "nginx" "proxyCachePath" "" "keysZoneName" ])
+    (mkRenamedOptionModule [ "services" "nginx" "proxyCache" "enable" ] [ "services" "nginx" "proxyCachePath" "" "enable" ])
   ];
 
   config = mkIf cfg.enable {
-    # TODO: test user supplied config file pases syntax test
-
     warnings =
     let
       deprecatedSSL = name: config: optional config.enableSSL
@@ -991,12 +1070,23 @@ in
           services.nginx.virtualHosts.<name>.useACMEHost are mutually exclusive.
         '';
       }
+
+      {
+        assertion = cfg.package.pname != "nginxQuic" -> all (host: !host.quic) (attrValues virtualHosts);
+        message = ''
+          services.nginx.service.virtualHosts.<name>.quic requires using nginxQuic package,
+          which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`.
+        '';
+      }
     ] ++ map (name: mkCertOwnershipAssertion {
       inherit (cfg) group user;
       cert = config.security.acme.certs.${name};
       groups = config.users.groups;
     }) dependentCertNames;
 
+    services.nginx.additionalModules = optional cfg.recommendedBrotliSettings pkgs.nginxModules.brotli
+      ++ lib.optional cfg.recommendedZstdSettings pkgs.nginxModules.zstd;
+
     systemd.services.nginx = {
       description = "Nginx Web Server";
       wantedBy = [ "multi-user.target" ];
@@ -1070,7 +1160,7 @@ in
     };
 
     environment.etc."nginx/nginx.conf" = mkIf cfg.enableReload {
-      source = finalConfigFile;
+      source = configFile;
     };
 
     # This service waits for all certificates to be available
@@ -1082,14 +1172,14 @@ in
       sslServices = map (certName: "acme-${certName}.service") dependentCertNames;
       sslTargets = map (certName: "acme-finished-${certName}.target") dependentCertNames;
     in mkIf (cfg.enableReload || sslServices != []) {
-      wants = optionals (cfg.enableReload) [ "nginx.service" ];
+      wants = optionals cfg.enableReload [ "nginx.service" ];
       wantedBy = sslServices ++ [ "multi-user.target" ];
       # Before the finished targets, after the renew services.
       # This service might be needed for HTTP-01 challenges, but we only want to confirm
       # certs are updated _after_ config has been reloaded.
       before = sslTargets;
       after = sslServices;
-      restartTriggers = optionals (cfg.enableReload) [ finalConfigFile ];
+      restartTriggers = optionals cfg.enableReload [ configFile ];
       # Block reloading if not all certs exist yet.
       # Happens when config changes add new vhosts/certs.
       unitConfig.ConditionPathExists = optionals (sslServices != []) (map (certName: certs.${certName}.directory + "/fullchain.pem") dependentCertNames);
diff --git a/nixos/modules/services/web-servers/nginx/vhost-options.nix b/nixos/modules/services/web-servers/nginx/vhost-options.nix
index 089decb5f43..5b33694cf2d 100644
--- a/nixos/modules/services/web-servers/nginx/vhost-options.nix
+++ b/nixos/modules/services/web-servers/nginx/vhost-options.nix
@@ -188,11 +188,11 @@ with lib;
       type = types.bool;
       default = true;
       description = lib.mdDoc ''
-        Whether to enable HTTP 2.
+        Whether to enable the HTTP/2 protocol.
         Note that (as of writing) due to nginx's implementation, to disable
-        HTTP 2 you have to disable it on all vhosts that use a given
+        HTTP/2 you have to disable it on all vhosts that use a given
         IP address / port.
-        If there is one server block configured to enable http2,then it is
+        If there is one server block configured to enable http2, then it is
         enabled for all server blocks on this IP.
         See https://stackoverflow.com/a/39466948/263061.
       '';
@@ -200,12 +200,42 @@ with lib;
 
     http3 = mkOption {
       type = types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        Whether to enable the HTTP/3 protocol.
+        This requires using `pkgs.nginxQuic` package
+        which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`
+        and activate the QUIC transport protocol
+        `services.nginx.virtualHosts.<name>.quic = true;`.
+        Note that HTTP/3 support is experimental and
+        *not* yet recommended for production.
+        Read more at https://quic.nginx.org/
+      '';
+    };
+
+    http3_hq = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to enable the HTTP/0.9 protocol negotiation used in QUIC interoperability tests.
+        This requires using `pkgs.nginxQuic` package
+        which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`
+        and activate the QUIC transport protocol
+        `services.nginx.virtualHosts.<name>.quic = true;`.
+        Note that special application protocol support is experimental and
+        *not* yet recommended for production.
+        Read more at https://quic.nginx.org/
+      '';
+    };
+
+    quic = mkOption {
+      type = types.bool;
       default = false;
       description = lib.mdDoc ''
-        Whether to enable HTTP 3.
+        Whether to enable the QUIC transport protocol.
         This requires using `pkgs.nginxQuic` package
         which can be achieved by setting `services.nginx.package = pkgs.nginxQuic;`.
-        Note that HTTP 3 support is experimental and
+        Note that QUIC support is experimental and
         *not* yet recommended for production.
         Read more at https://quic.nginx.org/
       '';
diff --git a/nixos/modules/services/web-servers/stargazer.nix b/nixos/modules/services/web-servers/stargazer.nix
new file mode 100644
index 00000000000..ddb9e7d8ba1
--- /dev/null
+++ b/nixos/modules/services/web-servers/stargazer.nix
@@ -0,0 +1,226 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.stargazer;
+  globalSection = ''
+    listen = ${lib.concatStringsSep " " cfg.listen}
+    connection-logging = ${lib.boolToString cfg.connectionLogging}
+    log-ip = ${lib.boolToString cfg.ipLog}
+    log-ip-partial = ${lib.boolToString cfg.ipLogPartial}
+    request-timeout = ${toString cfg.requestTimeout}
+    response-timeout = ${toString cfg.responseTimeout}
+
+    [:tls]
+    store = ${toString cfg.store}
+    organization = ${cfg.certOrg}
+    gen-certs = ${lib.boolToString cfg.genCerts}
+    regen-certs = ${lib.boolToString cfg.regenCerts}
+    ${lib.optionalString (cfg.certLifetime != "") "cert-lifetime = ${cfg.certLifetime}"}
+
+  '';
+  genINI = lib.generators.toINI { };
+  configFile = pkgs.writeText "config.ini" (lib.strings.concatStrings (
+    [ globalSection ] ++ (lib.lists.forEach cfg.routes (section:
+      let
+        name = section.route;
+        params = builtins.removeAttrs section [ "route" ];
+      in
+      genINI
+        {
+          "${name}" = params;
+        } + "\n"
+    ))
+  ));
+in
+{
+  options.services.stargazer = {
+    enable = lib.mkEnableOption (lib.mdDoc "Stargazer Gemini server");
+
+    listen = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
+      default = [ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]";
+      defaultText = lib.literalExpression ''[ "0.0.0.0" ] ++ lib.optional config.networking.enableIPv6 "[::0]"'';
+      example = lib.literalExpression ''[ "10.0.0.12" "[2002:a00:1::]" ]'';
+      description = lib.mdDoc ''
+        Address and port to listen on.
+      '';
+    };
+
+    connectionLogging = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = lib.mdDoc "Whether or not to log connections to stdout.";
+    };
+
+    ipLog = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc "Log client IP addresses in the connection log.";
+    };
+
+    ipLogPartial = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = lib.mdDoc "Log partial client IP addresses in the connection log.";
+    };
+
+    requestTimeout = lib.mkOption {
+      type = lib.types.int;
+      default = 5;
+      description = lib.mdDoc ''
+        Number of seconds to wait for the client to send a complete
+        request. Set to 0 to disable.
+      '';
+    };
+
+    responseTimeout = lib.mkOption {
+      type = lib.types.int;
+      default = 0;
+      description = lib.mdDoc ''
+        Number of seconds to wait for the client to send a complete
+        request and for stargazer to finish sending the response.
+        Set to 0 to disable.
+      '';
+    };
+
+    store = lib.mkOption {
+      type = lib.types.path;
+      default = /var/lib/gemini/certs;
+      description = lib.mdDoc ''
+        Path to the certificate store on disk. This should be a
+        persistent directory writable by Stargazer.
+      '';
+    };
+
+    certOrg = lib.mkOption {
+      type = lib.types.str;
+      default = "stargazer";
+      description = lib.mdDoc ''
+        The name of the organization responsible for the X.509
+        certificate's /O name.
+      '';
+    };
+
+    genCerts = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        Set to false to disable automatic certificate generation.
+        Use if you want to provide your own certs.
+      '';
+    };
+
+    regenCerts = lib.mkOption {
+      type = lib.types.bool;
+      default = true;
+      description = lib.mdDoc ''
+        Set to false to turn off automatic regeneration of expired certificates.
+        Use if you want to provide your own certs.
+      '';
+    };
+
+    certLifetime = lib.mkOption {
+      type = lib.types.str;
+      default = "";
+      description = lib.mdDoc ''
+        How long certs generated by Stargazer should live for.
+        Certs live forever by default.
+      '';
+      example = lib.literalExpression "\"1y\"";
+    };
+
+    routes = lib.mkOption {
+      type = lib.types.listOf
+        (lib.types.submodule {
+          freeformType = with lib.types; attrsOf (nullOr
+            (oneOf [
+              bool
+              int
+              float
+              str
+            ]) // {
+            description = "INI atom (null, bool, int, float or string)";
+          });
+          options.route = lib.mkOption {
+            type = lib.types.str;
+            description = lib.mdDoc "Route section name";
+          };
+        });
+      default = [ ];
+      description = lib.mdDoc ''
+        Routes that Stargazer should server.
+
+        Expressed as a list of attribute sets. Each set must have a key `route`
+        that becomes the section name for that route in the stargazer ini cofig.
+        The remaining keys and vaules become the parameters for that route.
+
+        [Refer to upstream docs for other params](https://git.sr.ht/~zethra/stargazer/tree/main/item/doc/stargazer.ini.5.txt)
+      '';
+      example = lib.literalExpression ''
+        [
+          {
+            route = "example.com";
+            root = "/srv/gemini/example.com"
+          }
+          {
+            route = "example.com:/man";
+            root = "/cgi-bin";
+            cgi = true;
+          }
+          {
+            route = "other.org~(.*)";
+            redirect = "gemini://example.com";
+            rewrite = "\1";
+          }
+        ]
+      '';
+    };
+
+    user = lib.mkOption {
+      type = lib.types.str;
+      default = "stargazer";
+      description = lib.mdDoc "User account under which stargazer runs.";
+    };
+
+    group = lib.mkOption {
+      type = lib.types.str;
+      default = "stargazer";
+      description = lib.mdDoc "Group account under which stargazer runs.";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.stargazer = {
+      description = "Stargazer gemini server";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        ExecStart = "${pkgs.stargazer}/bin/stargazer ${configFile}";
+        Restart = "always";
+        # User and group
+        User = cfg.user;
+        Group = cfg.group;
+      };
+    };
+
+    # Create default cert store
+    system.activationScripts.makeStargazerCertDir =
+      lib.optionalAttrs (cfg.store == /var/lib/gemini/certs) ''
+        mkdir -p /var/lib/gemini/certs
+        chown -R ${cfg.user}:${cfg.group} /var/lib/gemini/certs
+      '';
+
+    users.users = lib.optionalAttrs (cfg.user == "stargazer") {
+      stargazer = {
+        group = cfg.group;
+        isSystemUser = true;
+      };
+    };
+
+    users.groups = lib.optionalAttrs (cfg.group == "stargazer") {
+      stargazer = { };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ gaykitty ];
+}
diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix
index d8bfee547c7..4d2c36287be 100644
--- a/nixos/modules/services/web-servers/tomcat.nix
+++ b/nixos/modules/services/web-servers/tomcat.nix
@@ -234,11 +234,11 @@ in
           ln -sfn ${tomcat}/conf/$i ${cfg.baseDir}/conf/`basename $i`
         done
 
-        ${if cfg.extraConfigFiles != [] then ''
+        ${optionalString (cfg.extraConfigFiles != []) ''
           for i in ${toString cfg.extraConfigFiles}; do
             ln -sfn $i ${cfg.baseDir}/conf/`basename $i`
           done
-        '' else ""}
+        ''}
 
         # Create a modified catalina.properties file
         # Change all references from CATALINA_HOME to CATALINA_BASE and add support for shared libraries
@@ -345,7 +345,7 @@ in
 
           # Symlink all the given web applications files or paths into the webapps/ directory
           # of this virtual host
-          for i in "${if virtualHost ? webapps then toString virtualHost.webapps else ""}"; do
+          for i in "${optionalString (virtualHost ? webapps) (toString virtualHost.webapps)}"; do
             if [ -f $i ]; then
               # If the given web application is a file, symlink it into the webapps/ directory
               ln -sfn $i ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps/`basename $i`
diff --git a/nixos/modules/services/x11/desktop-managers/budgie.nix b/nixos/modules/services/x11/desktop-managers/budgie.nix
new file mode 100644
index 00000000000..2eff81750d9
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/budgie.nix
@@ -0,0 +1,238 @@
+{ lib, pkgs, config, utils, ... }:
+
+let
+  inherit (lib) concatMapStrings literalExpression mdDoc mkDefault mkEnableOption mkIf mkOption types;
+
+  cfg = config.services.xserver.desktopManager.budgie;
+
+  nixos-background-light = pkgs.nixos-artwork.wallpapers.nineish;
+  nixos-background-dark = pkgs.nixos-artwork.wallpapers.nineish-dark-gray;
+
+  nixos-gsettings-overrides = pkgs.budgie.budgie-gsettings-overrides.override {
+    inherit (cfg) extraGSettingsOverrides extraGSettingsOverridePackages;
+    inherit nixos-background-dark nixos-background-light;
+  };
+
+  nixos-background-info = pkgs.writeTextFile {
+    name = "nixos-background-info";
+    text = ''
+      <?xml version="1.0"?>
+      <!DOCTYPE wallpapers SYSTEM "gnome-wp-list.dtd">
+      <wallpapers>
+        <wallpaper deleted="false">
+          <name>Nineish</name>
+          <filename>${nixos-background-light.gnomeFilePath}</filename>
+          <options>zoom</options>
+          <shade_type>solid</shade_type>
+          <pcolor>#d1dcf8</pcolor>
+          <scolor>#e3ebfe</scolor>
+        </wallpaper>
+        <wallpaper deleted="false">
+          <name>Nineish Dark Gray</name>
+          <filename>${nixos-background-dark.gnomeFilePath}</filename>
+          <options>zoom</options>
+          <shade_type>solid</shade_type>
+          <pcolor>#151515</pcolor>
+          <scolor>#262626</scolor>
+        </wallpaper>
+      </wallpapers>
+    '';
+    destination = "/share/gnome-background-properties/nixos.xml";
+  };
+in {
+  options = {
+    services.xserver.desktopManager.budgie = {
+      enable = mkEnableOption (mdDoc "the Budgie desktop");
+
+      sessionPath = mkOption {
+        description = mdDoc "Additional list of packages to be added to the session search path. Useful for GSettings-conditional autostart.";
+        type = with types; listOf package;
+        example = literalExpression "[ pkgs.budgie.budgie-desktop-view ]";
+        default = [];
+      };
+
+      extraGSettingsOverrides = mkOption {
+        description = mdDoc "Additional GSettings overrides.";
+        type = types.lines;
+        default = "";
+      };
+
+      extraGSettingsOverridePackages = mkOption {
+        description = mdDoc "List of packages for which GSettings are overridden.";
+        type = with types; listOf path;
+        default = [];
+      };
+
+      extraPlugins = mkOption {
+        description = mdDoc "Extra plugins for the Budgie desktop";
+        type = with types; listOf package;
+        default = [];
+      };
+    };
+
+    environment.budgie.excludePackages = mkOption {
+      description = mdDoc "Which packages Budgie should exclude from the default environment.";
+      type = with types; listOf package;
+      default = [];
+      example = literalExpression "[ pkgs.mate-terminal ]";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.displayManager.sessionPackages = with pkgs; [
+      budgie.budgie-desktop
+    ];
+
+    services.xserver.displayManager.lightdm.greeters.slick = {
+      enable = mkDefault true;
+      theme = mkDefault { name = "Qogir"; package = pkgs.qogir-theme; };
+      iconTheme = mkDefault { name = "Qogir"; package = pkgs.qogir-icon-theme; };
+      cursorTheme = mkDefault { name = "Qogir"; package = pkgs.qogir-icon-theme; };
+    };
+
+    services.xserver.desktopManager.budgie.sessionPath = [ pkgs.budgie.budgie-desktop-view ];
+
+    environment.extraInit = ''
+      ${concatMapStrings (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
+      '') cfg.sessionPath}
+    '';
+
+    environment.systemPackages = with pkgs;
+      [
+        # Budgie Desktop.
+        budgie.budgie-backgrounds
+        budgie.budgie-control-center
+        (budgie.budgie-desktop-with-plugins.override { plugins = cfg.extraPlugins; })
+        budgie.budgie-desktop-view
+        budgie.budgie-screensaver
+
+        # Required by the Budgie Desktop session.
+        (gnome.gnome-session.override { gnomeShellSupport = false; })
+
+        # Required by Budgie Menu.
+        gnome-menus
+
+        # Required by Budgie Control Center.
+        gnome.zenity
+
+        # Provides `gsettings`.
+        glib
+
+        # Update user directories.
+        xdg-user-dirs
+      ]
+      ++ (utils.removePackagesByName [
+          cinnamon.nemo
+          mate.eom
+          mate.pluma
+          mate.atril
+          mate.engrampa
+          mate.mate-calc
+          mate.mate-terminal
+          mate.mate-system-monitor
+          vlc
+
+          # Desktop themes.
+          qogir-theme
+          qogir-icon-theme
+          nixos-background-info
+
+          # Default settings.
+          nixos-gsettings-overrides
+        ] config.environment.budgie.excludePackages)
+      ++ cfg.sessionPath;
+
+    # Fonts.
+    fonts.fonts = mkDefault [
+      pkgs.noto-fonts
+      pkgs.hack-font
+    ];
+    fonts.fontconfig.defaultFonts = {
+      sansSerif = mkDefault ["Noto Sans"];
+      monospace = mkDefault ["Hack"];
+    };
+
+    # Qt application style.
+    qt = {
+      enable = mkDefault true;
+      style = mkDefault "gtk2";
+      platformTheme = mkDefault "gtk2";
+    };
+
+    environment.pathsToLink = [
+      "/share" # TODO: https://github.com/NixOS/nixpkgs/issues/47173
+    ];
+
+    # GSettings overrides.
+    environment.sessionVariables.NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-overrides}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+
+    # Required by Budgie Desktop.
+    services.xserver.updateDbusEnvironment = true;
+    programs.dconf.enable = true;
+
+    # Required by Budgie Screensaver.
+    security.pam.services.budgie-screensaver = {};
+
+    # Required by Budgie's Polkit Dialog.
+    security.polkit.enable = mkDefault true;
+
+    # Required by Budgie Panel plugins and/or Budgie Control Center panels.
+    networking.networkmanager.enable = mkDefault true; # for BCC's Network panel.
+    programs.nm-applet.enable = config.networking.networkmanager.enable; # Budgie has no Network applet.
+    programs.nm-applet.indicator = false; # Budgie doesn't support AppIndicators.
+
+    hardware.bluetooth.enable = mkDefault true; # for Budgie's Status Indicator and BCC's Bluetooth panel.
+    hardware.pulseaudio.enable = mkDefault true; # for Budgie's Status Indicator and BCC's Sound panel.
+
+    xdg.portal.enable = mkDefault true; # for BCC's Applications panel.
+    xdg.portal.extraPortals = with pkgs; [
+      xdg-desktop-portal-gtk # provides a XDG Portals implementation.
+    ];
+
+    services.geoclue2.enable = mkDefault true; # for BCC's Privacy > Location Services panel.
+    services.upower.enable = config.powerManagement.enable; # for Budgie's Status Indicator and BCC's Power panel.
+    services.xserver.libinput.enable = mkDefault true; # for BCC's Mouse panel.
+    services.colord.enable = mkDefault true; # for BCC's Color panel.
+    services.gnome.at-spi2-core.enable = mkDefault true; # for BCC's A11y panel.
+    services.accounts-daemon.enable = mkDefault true; # for BCC's Users panel.
+    services.fprintd.enable = mkDefault true; # for BCC's Users panel.
+    services.udisks2.enable = mkDefault true; # for BCC's Details panel.
+
+    # For BCC's Online Accounts panel.
+    services.gnome.gnome-online-accounts.enable = mkDefault true;
+    services.gnome.gnome-online-miners.enable = true;
+
+    # For BCC's Printers panel.
+    services.printing.enable = mkDefault true;
+    services.system-config-printer.enable = config.services.printing.enable;
+
+    # For BCC's Sharing panel.
+    services.dleyna-renderer.enable = mkDefault true;
+    services.dleyna-server.enable = mkDefault true;
+    services.gnome.gnome-user-share.enable = mkDefault true;
+    services.gnome.rygel.enable = mkDefault true;
+
+    # Other default services.
+    services.gnome.evolution-data-server.enable = mkDefault true;
+    services.gnome.glib-networking.enable = mkDefault true;
+    services.gnome.gnome-keyring.enable = mkDefault true;
+    services.gnome.gnome-settings-daemon.enable = mkDefault true;
+    services.gvfs.enable = mkDefault true;
+
+    # Register packages for DBus.
+    services.dbus.packages = with pkgs; [
+      budgie.budgie-control-center
+    ];
+
+    # Shell integration for MATE Terminal.
+    programs.bash.vteIntegration = true;
+    programs.zsh.vteIntegration = true;
+  };
+}
diff --git a/nixos/modules/services/x11/desktop-managers/cinnamon.nix b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
index aaad1de5f87..2d8addb0f10 100644
--- a/nixos/modules/services/x11/desktop-managers/cinnamon.nix
+++ b/nixos/modules/services/x11/desktop-managers/cinnamon.nix
@@ -105,10 +105,11 @@ in
       services.dbus.packages = with pkgs.cinnamon; [
         cinnamon-common
         cinnamon-screensaver
-        nemo
+        nemo-with-extensions
         xapp
       ];
       services.cinnamon.apps.enable = mkDefault true;
+      services.gnome.evolution-data-server.enable = true;
       services.gnome.glib-networking.enable = true;
       services.gnome.gnome-keyring.enable = true;
       services.gvfs.enable = true;
@@ -154,7 +155,7 @@ in
         polkit_gnome
 
         # packages
-        nemo
+        nemo-with-extensions
         cinnamon-control-center
         cinnamon-settings-daemon
         libgnomekbd
@@ -180,6 +181,7 @@ in
         mint-themes
         mint-x-icons
         mint-y-icons
+        xapp # provides some xapp-* icons
       ] config.environment.cinnamon.excludePackages);
 
       xdg.mime.enable = true;
@@ -197,10 +199,10 @@ in
       programs.bash.vteIntegration = mkDefault true;
       programs.zsh.vteIntegration = mkDefault true;
 
-      # Harmonize Qt5 applications under Cinnamon
-      qt5.enable = true;
-      qt5.platformTheme = "gnome";
-      qt5.style = "adwaita";
+      # Harmonize Qt applications under Cinnamon
+      qt.enable = true;
+      qt.platformTheme = "gnome";
+      qt.style = "adwaita";
 
       # Default Fonts
       fonts.fonts = with pkgs; [
@@ -213,7 +215,6 @@ in
       programs.geary.enable = mkDefault true;
       programs.gnome-disks.enable = mkDefault true;
       programs.gnome-terminal.enable = mkDefault true;
-      programs.evince.enable = mkDefault true;
       programs.file-roller.enable = mkDefault true;
 
       environment.systemPackages = with pkgs // pkgs.gnome // pkgs.cinnamon; utils.removePackagesByName [
@@ -231,6 +232,7 @@ in
         # external apps shipped with linux-mint
         hexchat
         gnome-calculator
+        gnome-calendar
         gnome-screenshot
       ] config.environment.cinnamon.excludePackages;
     })
diff --git a/nixos/modules/services/x11/desktop-managers/deepin.nix b/nixos/modules/services/x11/desktop-managers/deepin.nix
new file mode 100644
index 00000000000..70be6ed7c05
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/deepin.nix
@@ -0,0 +1,208 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+
+let
+  xcfg = config.services.xserver;
+  cfg = xcfg.desktopManager.deepin;
+
+  nixos-gsettings-overrides = pkgs.deepin.dde-gsettings-schemas.override {
+    extraGSettingsOverridePackages = cfg.extraGSettingsOverridePackages;
+    extraGSettingsOverrides = cfg.extraGSettingsOverrides;
+  };
+in
+{
+  options = {
+
+    services.xserver.desktopManager.deepin = {
+      enable = mkEnableOption (lib.mdDoc "Enable Deepin desktop manager");
+      extraGSettingsOverrides = mkOption {
+        default = "";
+        type = types.lines;
+        description = lib.mdDoc "Additional gsettings overrides.";
+      };
+      extraGSettingsOverridePackages = mkOption {
+        default = [ ];
+        type = types.listOf types.path;
+        description = lib.mdDoc "List of packages for which gsettings are overridden.";
+      };
+    };
+
+    environment.deepin.excludePackages = mkOption {
+      default = [ ];
+      type = types.listOf types.package;
+      description = lib.mdDoc "List of default packages to exclude from the configuration";
+    };
+
+  };
+
+  config = mkIf cfg.enable
+    {
+      services.xserver.displayManager.sessionPackages = [ pkgs.deepin.startdde ];
+      services.xserver.displayManager.defaultSession = mkDefault "deepin";
+
+      # Update the DBus activation environment after launching the desktop manager.
+      services.xserver.displayManager.sessionCommands = ''
+        ${lib.getBin pkgs.dbus}/bin/dbus-update-activation-environment --systemd --all
+      '';
+
+      hardware.bluetooth.enable = mkDefault true;
+      hardware.pulseaudio.enable = mkDefault true;
+      security.polkit.enable = true;
+
+      services.deepin.dde-daemon.enable = mkForce true;
+      services.deepin.dde-api.enable = mkForce true;
+      services.deepin.app-services.enable = mkForce true;
+
+      services.colord.enable = mkDefault true;
+      services.accounts-daemon.enable = mkDefault true;
+      services.gvfs.enable = mkDefault true;
+      services.gnome.glib-networking.enable = mkDefault true;
+      services.gnome.gnome-keyring.enable = mkDefault true;
+      services.bamf.enable = mkDefault true;
+
+      services.xserver.libinput.enable = mkDefault true;
+      services.udisks2.enable = true;
+      services.upower.enable = mkDefault config.powerManagement.enable;
+      networking.networkmanager.enable = mkDefault true;
+      programs.dconf.enable = mkDefault true;
+
+      fonts.fonts = with pkgs; [ noto-fonts ];
+      xdg.mime.enable = true;
+      xdg.menus.enable = true;
+      xdg.icons.enable = true;
+      xdg.portal.enable = mkDefault true;
+      xdg.portal.extraPortals = mkDefault [
+        (pkgs.xdg-desktop-portal-gtk.override {
+          buildPortalsInGnome = false;
+        })
+      ];
+
+      environment.sessionVariables = {
+        NIX_GSETTINGS_OVERRIDES_DIR = "${nixos-gsettings-overrides}/share/gsettings-schemas/nixos-gsettings-overrides/glib-2.0/schemas";
+        DDE_POLKIT_AGENT_PLUGINS_DIRS = [ "${pkgs.deepin.dpa-ext-gnomekeyring}/lib/polkit-1-dde/plugins" ];
+      };
+
+      environment.pathsToLink = [
+        "/lib/dde-dock/plugins"
+        "/lib/dde-control-center"
+        "/lib/dde-session-shell"
+        "/lib/dde-file-manager"
+        "/share/backgrounds"
+        "/share/wallpapers"
+      ];
+
+      environment.etc = {
+        "distribution.info".text = ''
+          [Distribution]
+          Name=NixOS
+          WebsiteName=www.nixos.org
+          Website=https://www.nixos.org
+          Logo=${pkgs.nixos-icons}/share/icons/hicolor/96x96/apps/nix-snowflake.png
+          LogoLight=${pkgs.nixos-icons}/share/icons/hicolor/32x32/apps/nix-snowflake.png
+          LogoTransparent=${pkgs.deepin.deepin-desktop-base}/share/pixmaps/distribution_logo_transparent.svg
+        '';
+        "deepin-installer.conf".text = ''
+          system_info_vendor_name="Copyright (c) 2003-2023 NixOS contributors"
+        '';
+      };
+
+      systemd.tmpfiles.rules = [
+        "d /var/lib/AccountsService 0775 root root - -"
+        "C /var/lib/AccountsService/icons 0775 root root - ${pkgs.deepin.dde-account-faces}/var/lib/AccountsService/icons"
+      ];
+
+      security.pam.services.dde-lock.text = ''
+        # original at {dde-session-shell}/etc/pam.d/dde-lock
+        auth      substack      login
+        account   include       login
+        password  substack      login
+        session   include       login
+      '';
+
+      environment.systemPackages = with pkgs; with deepin;
+        let
+          requiredPackages = [
+            pciutils # for dtkcore/startdde
+            xdotool # for dde-daemon
+            glib # for gsettings program / gdbus
+            gtk3 # for gtk-launch program
+            xdg-user-dirs # Update user dirs
+            util-linux # runuser
+            polkit_gnome
+            librsvg # dde-api use rsvg-convert
+            lshw # for dtkcore
+            libsForQt5.kde-gtk-config # deepin-api/gtk-thumbnailer need
+            libsForQt5.kglobalaccel
+            xsettingsd # lightdm-deepin-greeter
+            qt5platform-plugins
+            deepin-pw-check
+            deepin-turbo
+
+            dde-account-faces
+            deepin-icon-theme
+            deepin-sound-theme
+            deepin-gtk-theme
+            deepin-wallpapers
+
+            startdde
+            dde-dock
+            dde-launcher
+            dde-session-ui
+            dde-session-shell
+            dde-file-manager
+            dde-control-center
+            dde-network-core
+            dde-clipboard
+            dde-calendar
+            dde-polkit-agent
+            dpa-ext-gnomekeyring
+            deepin-desktop-schemas
+            deepin-terminal
+            dde-kwin
+            deepin-kwin
+          ];
+          optionalPackages = [
+            onboard # dde-dock plugin
+            deepin-camera
+            deepin-calculator
+            deepin-compressor
+            deepin-editor
+            deepin-picker
+            deepin-draw
+            deepin-album
+            deepin-image-viewer
+            deepin-music
+            deepin-movie-reborn
+            deepin-system-monitor
+            deepin-screen-recorder
+            deepin-shortcut-viewer
+          ];
+        in
+        requiredPackages
+        ++ utils.removePackagesByName optionalPackages config.environment.deepin.excludePackages;
+
+      services.dbus.packages = with pkgs.deepin; [
+        dde-dock
+        dde-launcher
+        dde-session-ui
+        dde-session-shell
+        dde-file-manager
+        dde-control-center
+        dde-calendar
+        dde-clipboard
+        dde-kwin
+        deepin-kwin
+        deepin-pw-check
+      ];
+
+      systemd.packages = with pkgs.deepin; [
+        dde-launcher
+        dde-file-manager
+        dde-calendar
+        dde-clipboard
+        deepin-kwin
+      ];
+    };
+}
+
diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix
index 510561246a2..66cb4ee29c0 100644
--- a/nixos/modules/services/x11/desktop-managers/default.nix
+++ b/nixos/modules/services/x11/desktop-managers/default.nix
@@ -21,7 +21,7 @@ in
     ./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
+    ./cinnamon.nix ./budgie.nix ./deepin.nix
   ];
 
   options = {
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.md b/nixos/modules/services/x11/desktop-managers/gnome.md
new file mode 100644
index 00000000000..d9e75bfe6bd
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/gnome.md
@@ -0,0 +1,167 @@
+# GNOME Desktop {#chap-gnome}
+
+GNOME provides a simple, yet full-featured desktop environment with a focus on productivity. Its Mutter compositor supports both Wayland and X server, and the GNOME Shell user interface is fully customizable by extensions.
+
+## Enabling GNOME {#sec-gnome-enable}
+
+All of the core apps, optional apps, games, and core developer tools from GNOME are available.
+
+To enable the GNOME desktop use:
+
+```
+services.xserver.desktopManager.gnome.enable = true;
+services.xserver.displayManager.gdm.enable = true;
+```
+
+::: {.note}
+While it is not strictly necessary to use GDM as the display manager with GNOME, it is recommended, as some features such as screen lock [might not work](#sec-gnome-faq-can-i-use-lightdm-with-gnome) without it.
+:::
+
+The default applications used in NixOS are very minimal, inspired by the defaults used in [gnome-build-meta](https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/40.0/elements/core/meta-gnome-core-utilities.bst).
+
+### GNOME without the apps {#sec-gnome-without-the-apps}
+
+If you’d like to only use the GNOME desktop and not the apps, you can disable them with:
+
+```
+services.gnome.core-utilities.enable = false;
+```
+
+and none of them will be installed.
+
+If you’d only like to omit a subset of the core utilities, you can use
+[](#opt-environment.gnome.excludePackages).
+Note that this mechanism can only exclude core utilities, games and core developer tools.
+
+### Disabling GNOME services {#sec-gnome-disabling-services}
+
+It is also possible to disable many of the [core services](https://github.com/NixOS/nixpkgs/blob/b8ec4fd2a4edc4e30d02ba7b1a2cc1358f3db1d5/nixos/modules/services/x11/desktop-managers/gnome.nix#L329-L348). For example, if you do not need indexing files, you can disable Tracker with:
+
+```
+services.gnome.tracker-miners.enable = false;
+services.gnome.tracker.enable = false;
+```
+
+Note, however, that doing so is not supported and might break some applications. Notably, GNOME Music cannot work without Tracker.
+
+### GNOME games {#sec-gnome-games}
+
+You can install all of the GNOME games with:
+
+```
+services.gnome.games.enable = true;
+```
+
+### GNOME core developer tools {#sec-gnome-core-developer-tools}
+
+You can install GNOME core developer tools with:
+
+```
+services.gnome.core-developer-tools.enable = true;
+```
+
+## Enabling GNOME Flashback {#sec-gnome-enable-flashback}
+
+GNOME Flashback provides a desktop environment based on the classic GNOME 2 architecture. You can enable the default GNOME Flashback session, which uses the Metacity window manager, with:
+
+```
+services.xserver.desktopManager.gnome.flashback.enableMetacity = true;
+```
+
+It is also possible to create custom sessions that replace Metacity with a different window manager using [](#opt-services.xserver.desktopManager.gnome.flashback.customSessions).
+
+The following example uses `xmonad` window manager:
+
+```
+services.xserver.desktopManager.gnome.flashback.customSessions = [
+  {
+    wmName = "xmonad";
+    wmLabel = "XMonad";
+    wmCommand = "${pkgs.haskellPackages.xmonad}/bin/xmonad";
+    enableGnomePanel = false;
+  }
+];
+```
+
+## Icons and GTK Themes {#sec-gnome-icons-and-gtk-themes}
+
+Icon themes and GTK themes don’t require any special option to install in NixOS.
+
+You can add them to [](#opt-environment.systemPackages) and switch to them with GNOME Tweaks.
+If you’d like to do this manually in dconf, change the values of the following keys:
+
+```
+/org/gnome/desktop/interface/gtk-theme
+/org/gnome/desktop/interface/icon-theme
+```
+
+in `dconf-editor`
+
+## Shell Extensions {#sec-gnome-shell-extensions}
+
+Most Shell extensions are packaged under the `gnomeExtensions` attribute.
+Some packages that include Shell extensions, like `gnome.gpaste`, don’t have their extension decoupled under this attribute.
+
+You can install them like any other package:
+
+```
+environment.systemPackages = [
+  gnomeExtensions.dash-to-dock
+  gnomeExtensions.gsconnect
+  gnomeExtensions.mpris-indicator-button
+];
+```
+
+Unfortunately, we lack a way for these to be managed in a completely declarative way.
+So you have to enable them manually with an Extensions application.
+It is possible to use a [GSettings override](#sec-gnome-gsettings-overrides) for this on `org.gnome.shell.enabled-extensions`, but that will only influence the default value.
+
+## GSettings Overrides {#sec-gnome-gsettings-overrides}
+
+Majority of software building on the GNOME platform use GLib’s [GSettings](https://developer.gnome.org/gio/unstable/GSettings.html) system to manage runtime configuration. For our purposes, the system consists of XML schemas describing the individual configuration options, stored in the package, and a settings backend, where the values of the settings are stored. On NixOS, like on most Linux distributions, dconf database is used as the backend.
+
+[GSettings vendor overrides](https://developer.gnome.org/gio/unstable/GSettings.html#id-1.4.19.2.9.25) can be used to adjust the default values for settings of the GNOME desktop and apps by replacing the default values specified in the XML schemas. Using overrides will allow you to pre-seed user settings before you even start the session.
+
+::: {.warning}
+Overrides really only change the default values for GSettings keys so if you or an application changes the setting value, the value set by the override will be ignored. Until [NixOS’s dconf module implements changing values](https://github.com/NixOS/nixpkgs/issues/54150), you will either need to keep that in mind and clear the setting from the backend using `dconf reset` command when that happens, or use the [module from home-manager](https://nix-community.github.io/home-manager/options.html#opt-dconf.settings).
+:::
+
+You can override the default GSettings values using the
+[](#opt-services.xserver.desktopManager.gnome.extraGSettingsOverrides) option.
+
+Take note that whatever packages you want to override GSettings for, you need to add them to
+[](#opt-services.xserver.desktopManager.gnome.extraGSettingsOverridePackages).
+
+You can use `dconf-editor` tool to explore which GSettings you can set.
+
+### Example {#sec-gnome-gsettings-overrides-example}
+
+```
+services.xserver.desktopManager.gnome = {
+  extraGSettingsOverrides = ''
+    # Change default background
+    [org.gnome.desktop.background]
+    picture-uri='file://${pkgs.nixos-artwork.wallpapers.mosaic-blue.gnomeFilePath}'
+
+    # Favorite apps in gnome-shell
+    [org.gnome.shell]
+    favorite-apps=['org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop']
+  '';
+
+  extraGSettingsOverridePackages = [
+    pkgs.gsettings-desktop-schemas # for org.gnome.desktop
+    pkgs.gnome.gnome-shell # for org.gnome.shell
+  ];
+};
+```
+
+## Frequently Asked Questions {#sec-gnome-faq}
+
+### Can I use LightDM with GNOME? {#sec-gnome-faq-can-i-use-lightdm-with-gnome}
+
+Yes you can, and any other display-manager in NixOS.
+
+However, it doesn’t work correctly for the Wayland session of GNOME Shell yet, and
+won’t be able to lock your screen.
+
+See [this issue.](https://github.com/NixOS/nixpkgs/issues/56342)
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.nix b/nixos/modules/services/x11/desktop-managers/gnome.nix
index 9c1978e362b..79b2e7c6ead 100644
--- a/nixos/modules/services/x11/desktop-managers/gnome.nix
+++ b/nixos/modules/services/x11/desktop-managers/gnome.nix
@@ -66,7 +66,7 @@ in
 {
 
   meta = {
-    doc = ./gnome.xml;
+    doc = ./gnome.md;
     maintainers = teams.gnome.members;
   };
 
@@ -352,8 +352,8 @@ in
         })
       ];
 
-      # Harmonize Qt5 application style and also make them use the portal for file chooser dialog.
-      qt5 = {
+      # Harmonize Qt application style and also make them use the portal for file chooser dialog.
+      qt = {
         enable = mkDefault true;
         platformTheme = mkDefault "gnome";
         style = mkDefault "adwaita";
diff --git a/nixos/modules/services/x11/desktop-managers/gnome.xml b/nixos/modules/services/x11/desktop-managers/gnome.xml
deleted file mode 100644
index 807c9d64e20..00000000000
--- a/nixos/modules/services/x11/desktop-managers/gnome.xml
+++ /dev/null
@@ -1,253 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         xml:id="chap-gnome">
- <title>GNOME Desktop</title>
- <para>
-  GNOME provides a simple, yet full-featured desktop environment with a focus on productivity. Its Mutter compositor supports both Wayland and X server, and the GNOME Shell user interface is fully customizable by extensions.
- </para>
-
- <section xml:id="sec-gnome-enable">
-  <title>Enabling GNOME</title>
-
-  <para>
-   All of the core apps, optional apps, games, and core developer tools from GNOME are available.
-  </para>
-
-  <para>
-   To enable the GNOME desktop use:
-  </para>
-
-<programlisting>
-<xref linkend="opt-services.xserver.desktopManager.gnome.enable"/> = true;
-<xref linkend="opt-services.xserver.displayManager.gdm.enable"/> = true;
-</programlisting>
-
-  <note>
-   <para>
-    While it is not strictly necessary to use GDM as the display manager with GNOME, it is recommended, as some features such as screen lock <link xlink:href="#sec-gnome-faq-can-i-use-lightdm-with-gnome">might not work</link> without it.
-   </para>
-  </note>
-
-  <para>
-   The default applications used in NixOS are very minimal, inspired by the defaults used in <link xlink:href="https://gitlab.gnome.org/GNOME/gnome-build-meta/blob/40.0/elements/core/meta-gnome-core-utilities.bst">gnome-build-meta</link>.
-  </para>
-
-  <section xml:id="sec-gnome-without-the-apps">
-   <title>GNOME without the apps</title>
-
-   <para>
-    If you’d like to only use the GNOME desktop and not the apps, you can disable them with:
-   </para>
-
-<programlisting>
-<xref linkend="opt-services.gnome.core-utilities.enable"/> = false;
-</programlisting>
-
-   <para>
-    and none of them will be installed.
-   </para>
-
-   <para>
-    If you’d only like to omit a subset of the core utilities, you can use <xref linkend="opt-environment.gnome.excludePackages"/>.
-    Note that this mechanism can only exclude core utilities, games and core developer tools.
-   </para>
-  </section>
-
-  <section xml:id="sec-gnome-disabling-services">
-   <title>Disabling GNOME services</title>
-
-   <para>
-    It is also possible to disable many of the <link xlink:href="https://github.com/NixOS/nixpkgs/blob/b8ec4fd2a4edc4e30d02ba7b1a2cc1358f3db1d5/nixos/modules/services/x11/desktop-managers/gnome.nix#L329-L348">core services</link>. For example, if you do not need indexing files, you can disable Tracker with:
-   </para>
-
-<programlisting>
-<xref linkend="opt-services.gnome.tracker-miners.enable"/> = false;
-<xref linkend="opt-services.gnome.tracker.enable"/> = false;
-</programlisting>
-
-   <para>
-    Note, however, that doing so is not supported and might break some applications. Notably, GNOME Music cannot work without Tracker.
-   </para>
-  </section>
-
-  <section xml:id="sec-gnome-games">
-   <title>GNOME games</title>
-
-   <para>
-    You can install all of the GNOME games with:
-   </para>
-
-<programlisting>
-<xref linkend="opt-services.gnome.games.enable"/> = true;
-</programlisting>
-  </section>
-
-  <section xml:id="sec-gnome-core-developer-tools">
-   <title>GNOME core developer tools</title>
-
-   <para>
-    You can install GNOME core developer tools with:
-   </para>
-
-<programlisting>
-<xref linkend="opt-services.gnome.core-developer-tools.enable"/> = true;
-</programlisting>
-  </section>
- </section>
-
- <section xml:id="sec-gnome-enable-flashback">
-  <title>Enabling GNOME Flashback</title>
-
-  <para>
-   GNOME Flashback provides a desktop environment based on the classic GNOME 2 architecture. You can enable the default GNOME Flashback session, which uses the Metacity window manager, with:
-  </para>
-
-<programlisting>
-<xref linkend="opt-services.xserver.desktopManager.gnome.flashback.enableMetacity"/> = true;
-</programlisting>
-
-  <para>
-   It is also possible to create custom sessions that replace Metacity with a different window manager using <xref linkend="opt-services.xserver.desktopManager.gnome.flashback.customSessions"/>.
-  </para>
-
-  <para>
-   The following example uses <literal>xmonad</literal> window manager:
-  </para>
-
-<programlisting>
-<xref linkend="opt-services.xserver.desktopManager.gnome.flashback.customSessions"/> = [
-  {
-    wmName = "xmonad";
-    wmLabel = "XMonad";
-    wmCommand = "${pkgs.haskellPackages.xmonad}/bin/xmonad";
-    enableGnomePanel = false;
-  }
-];
-</programlisting>
-
- </section>
-
- <section xml:id="sec-gnome-icons-and-gtk-themes">
-  <title>Icons and GTK Themes</title>
-
-  <para>
-   Icon themes and GTK themes don’t require any special option to install in NixOS.
-  </para>
-
-  <para>
-   You can add them to <xref linkend="opt-environment.systemPackages"/> and switch to them with GNOME Tweaks.
-   If you’d like to do this manually in dconf, change the values of the following keys:
-  </para>
-
-<programlisting>
-/org/gnome/desktop/interface/gtk-theme
-/org/gnome/desktop/interface/icon-theme
-</programlisting>
-
-  <para>
-   in <literal>dconf-editor</literal>
-  </para>
- </section>
-
- <section xml:id="sec-gnome-shell-extensions">
-  <title>Shell Extensions</title>
-
-  <para>
-   Most Shell extensions are packaged under the <literal>gnomeExtensions</literal> attribute.
-   Some packages that include Shell extensions, like <literal>gnome.gpaste</literal>, don’t have their extension decoupled under this attribute.
-  </para>
-
-  <para>
-   You can install them like any other package:
-  </para>
-
-<programlisting>
-<xref linkend="opt-environment.systemPackages"/> = [
-  gnomeExtensions.dash-to-dock
-  gnomeExtensions.gsconnect
-  gnomeExtensions.mpris-indicator-button
-];
-</programlisting>
-
-  <para>
-   Unfortunately, we lack a way for these to be managed in a completely declarative way.
-   So you have to enable them manually with an Extensions application.
-   It is possible to use a <link xlink:href="#sec-gnome-gsettings-overrides">GSettings override</link> for this on <literal>org.gnome.shell.enabled-extensions</literal>, but that will only influence the default value.
-  </para>
- </section>
-
- <section xml:id="sec-gnome-gsettings-overrides">
-  <title>GSettings Overrides</title>
-
-  <para>
-   Majority of software building on the GNOME platform use GLib’s <link xlink:href="https://developer.gnome.org/gio/unstable/GSettings.html">GSettings</link> system to manage runtime configuration. For our purposes, the system consists of XML schemas describing the individual configuration options, stored in the package, and a settings backend, where the values of the settings are stored. On NixOS, like on most Linux distributions, dconf database is used as the backend.
-  </para>
-
-  <para>
-   <link xlink:href="https://developer.gnome.org/gio/unstable/GSettings.html#id-1.4.19.2.9.25">GSettings vendor overrides</link> can be used to adjust the default values for settings of the GNOME desktop and apps by replacing the default values specified in the XML schemas. Using overrides will allow you to pre-seed user settings before you even start the session.
-  </para>
-
-  <warning>
-   <para>
-    Overrides really only change the default values for GSettings keys so if you or an application changes the setting value, the value set by the override will be ignored. Until <link xlink:href="https://github.com/NixOS/nixpkgs/issues/54150">NixOS’s dconf module implements changing values</link>, you will either need to keep that in mind and clear the setting from the backend using <literal>dconf reset</literal> command when that happens, or use the <link xlink:href="https://nix-community.github.io/home-manager/options.html#opt-dconf.settings">module from home-manager</link>.
-   </para>
-  </warning>
-
-  <para>
-   You can override the default GSettings values using the <xref linkend="opt-services.xserver.desktopManager.gnome.extraGSettingsOverrides"/> option.
-  </para>
-
-  <para>
-   Take note that whatever packages you want to override GSettings for, you need to add them to
-   <xref linkend="opt-services.xserver.desktopManager.gnome.extraGSettingsOverridePackages"/>.
-  </para>
-
-  <para>
-   You can use <literal>dconf-editor</literal> tool to explore which GSettings you can set.
-  </para>
-
-  <section xml:id="sec-gnome-gsettings-overrides-example">
-   <title>Example</title>
-
-<programlisting>
-services.xserver.desktopManager.gnome = {
-  <link xlink:href="#opt-services.xserver.desktopManager.gnome.extraGSettingsOverrides">extraGSettingsOverrides</link> = ''
-    # Change default background
-    [org.gnome.desktop.background]
-    picture-uri='file://${pkgs.nixos-artwork.wallpapers.mosaic-blue.gnomeFilePath}'
-
-    # Favorite apps in gnome-shell
-    [org.gnome.shell]
-    favorite-apps=['org.gnome.Photos.desktop', 'org.gnome.Nautilus.desktop']
-  '';
-
-  <link xlink:href="#opt-services.xserver.desktopManager.gnome.extraGSettingsOverridePackages">extraGSettingsOverridePackages</link> = [
-    pkgs.gsettings-desktop-schemas # for org.gnome.desktop
-    pkgs.gnome.gnome-shell # for org.gnome.shell
-  ];
-};
-</programlisting>
-  </section>
- </section>
-
- <section xml:id="sec-gnome-faq">
-  <title>Frequently Asked Questions</title>
-
-  <section xml:id="sec-gnome-faq-can-i-use-lightdm-with-gnome">
-   <title>Can I use LightDM with GNOME?</title>
-
-   <para>
-    Yes you can, and any other display-manager in NixOS.
-   </para>
-
-   <para>
-    However, it doesn’t work correctly for the Wayland session of GNOME Shell yet, and
-    won’t be able to lock your screen.
-   </para>
-
-   <para>
-    See <link xlink:href="https://github.com/NixOS/nixpkgs/issues/56342">this issue.</link>
-   </para>
-  </section>
- </section>
-</chapter>
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.md b/nixos/modules/services/x11/desktop-managers/pantheon.md
new file mode 100644
index 00000000000..1c14ede8474
--- /dev/null
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.md
@@ -0,0 +1,74 @@
+# Pantheon Desktop {#chap-pantheon}
+
+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.
+
+## Enabling Pantheon {#sec-pantheon-enable}
+
+All of Pantheon is working in NixOS and the applications should be available, aside from a few [exceptions](https://github.com/NixOS/nixpkgs/issues/58161). To enable Pantheon, set
+```
+services.xserver.desktopManager.pantheon.enable = true;
+```
+This automatically enables LightDM and Pantheon's LightDM greeter. If you'd like to disable this, set
+```
+services.xserver.displayManager.lightdm.greeters.pantheon.enable = false;
+services.xserver.displayManager.lightdm.enable = false;
+```
+but please be aware using Pantheon without LightDM as a display manager will break screenlocking from the UI. The NixOS module for Pantheon installs all of Pantheon's default applications. If you'd like to not install Pantheon's apps, set
+```
+services.pantheon.apps.enable = false;
+```
+You can also use [](#opt-environment.pantheon.excludePackages) to remove any other app (like `elementary-mail`).
+
+## Wingpanel and Switchboard plugins {#sec-pantheon-wingpanel-switchboard}
+
+Wingpanel and Switchboard work differently than they do in other distributions, as far as using plugins. You cannot install a plugin globally (like with {option}`environment.systemPackages`) to start using it. You should instead be using the following options:
+
+  - [](#opt-services.xserver.desktopManager.pantheon.extraWingpanelIndicators)
+  - [](#opt-services.xserver.desktopManager.pantheon.extraSwitchboardPlugs)
+
+to configure the programs with plugs or indicators.
+
+The difference in NixOS is both these programs are patched to load plugins from a directory that is the value of an environment variable. All of which is controlled in Nix. If you need to configure the particular packages manually you can override the packages like:
+```
+wingpanel-with-indicators.override {
+  indicators = [
+    pkgs.some-special-indicator
+  ];
+};
+
+switchboard-with-plugs.override {
+  plugs = [
+    pkgs.some-special-plug
+  ];
+};
+```
+please note that, like how the NixOS options describe these as extra plugins, this would only add to the default plugins included with the programs. If for some reason you'd like to configure which plugins to use exactly, both packages have an argument for this:
+```
+wingpanel-with-indicators.override {
+  useDefaultIndicators = false;
+  indicators = specialListOfIndicators;
+};
+
+switchboard-with-plugs.override {
+  useDefaultPlugs = false;
+  plugs = specialListOfPlugs;
+};
+```
+this could be most useful for testing a particular plug-in in isolation.
+
+## FAQ {#sec-pantheon-faq}
+
+[I have switched from a different desktop and Pantheon’s theming looks messed up.]{#sec-pantheon-faq-messed-up-theme}
+  : Open Switchboard and go to: Administration → About → Restore Default Settings → Restore Settings. This will reset any dconf settings to their Pantheon defaults. Note this could reset certain GNOME specific preferences if that desktop was used prior.
+
+[I cannot enable both GNOME and Pantheon.]{#sec-pantheon-faq-gnome-and-pantheon}
+  : This is a known [issue](https://github.com/NixOS/nixpkgs/issues/64611) and there is no known workaround.
+
+[Does AppCenter work, or is it available?]{#sec-pantheon-faq-appcenter}
+  : AppCenter has been available since 20.03. Starting from 21.11, the Flatpak backend should work so you can install some Flatpak applications using it. However, due to missing appstream metadata, the Packagekit backend does not function currently. See this [issue](https://github.com/NixOS/nixpkgs/issues/15932).
+
+    If you are using Pantheon, AppCenter should be installed by default if you have [Flatpak support](#module-services-flatpak) enabled. If you also wish to add the `appcenter` Flatpak remote:
+
+    ```ShellSession
+    $ flatpak remote-add --if-not-exists appcenter https://flatpak.elementary.io/repo.flatpakrepo
+    ```
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix
index 5c0203224e1..ab4d75b1013 100644
--- a/nixos/modules/services/x11/desktop-managers/pantheon.nix
+++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix
@@ -17,7 +17,7 @@ in
 {
 
   meta = {
-    doc = ./pantheon.xml;
+    doc = ./pantheon.md;
     maintainers = teams.pantheon.members;
   };
 
@@ -169,6 +169,9 @@ in
       };
       services.udev.packages = [
         pkgs.pantheon.gnome-settings-daemon
+        # Force enable KMS modifiers for devices that require them.
+        # https://gitlab.gnome.org/GNOME/mutter/-/merge_requests/1443
+        pkgs.pantheon.mutter
       ];
       systemd.packages = [
         pkgs.pantheon.gnome-settings-daemon
@@ -250,10 +253,10 @@ in
       programs.bash.vteIntegration = mkDefault true;
       programs.zsh.vteIntegration = mkDefault true;
 
-      # Harmonize Qt5 applications under Pantheon
-      qt5.enable = true;
-      qt5.platformTheme = "gnome";
-      qt5.style = "adwaita";
+      # Harmonize Qt applications under Pantheon
+      qt.enable = true;
+      qt.platformTheme = "gnome";
+      qt.style = "adwaita";
 
       # Default Fonts
       fonts.fonts = with pkgs; [
@@ -285,7 +288,7 @@ in
         elementary-music
         elementary-photos
         elementary-screenshot
-        # elementary-tasks
+        elementary-tasks
         elementary-terminal
         elementary-videos
         epiphany
diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.xml b/nixos/modules/services/x11/desktop-managers/pantheon.xml
deleted file mode 100644
index 6226f8f6a27..00000000000
--- a/nixos/modules/services/x11/desktop-managers/pantheon.xml
+++ /dev/null
@@ -1,120 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook"
-         xmlns:xlink="http://www.w3.org/1999/xlink"
-         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 and Granite.
- </para>
- <section xml:id="sec-pantheon-enable">
-  <title>Enabling Pantheon</title>
-
-  <para>
-   All of Pantheon is working in NixOS and the applications should be available, aside from a few <link xlink:href="https://github.com/NixOS/nixpkgs/issues/58161">exceptions</link>. To enable Pantheon, set
-<programlisting>
-<xref linkend="opt-services.xserver.desktopManager.pantheon.enable"/> = true;
-</programlisting>
-   This automatically enables LightDM and Pantheon's LightDM greeter. If you'd like to disable this, set
-<programlisting>
-<xref linkend="opt-services.xserver.displayManager.lightdm.greeters.pantheon.enable"/> = false;
-<xref linkend="opt-services.xserver.displayManager.lightdm.enable"/> = false;
-</programlisting>
-   but please be aware using Pantheon without LightDM as a display manager will break screenlocking from the UI. The NixOS module for Pantheon installs all of Pantheon's default applications. If you'd like to not install Pantheon's apps, set
-<programlisting>
-<xref linkend="opt-services.pantheon.apps.enable"/> = false;
-</programlisting>
-   You can also use <xref linkend="opt-environment.pantheon.excludePackages"/> to remove any other app (like <package>elementary-mail</package>).
-  </para>
- </section>
- <section xml:id="sec-pantheon-wingpanel-switchboard">
-  <title>Wingpanel and Switchboard plugins</title>
-
-  <para>
-   Wingpanel and Switchboard work differently than they do in other distributions, as far as using plugins. You cannot install a plugin globally (like with <option>environment.systemPackages</option>) to start using it. You should instead be using the following options:
-   <itemizedlist>
-    <listitem>
-     <para>
-      <xref linkend="opt-services.xserver.desktopManager.pantheon.extraWingpanelIndicators"/>
-     </para>
-    </listitem>
-    <listitem>
-     <para>
-      <xref linkend="opt-services.xserver.desktopManager.pantheon.extraSwitchboardPlugs"/>
-     </para>
-    </listitem>
-   </itemizedlist>
-   to configure the programs with plugs or indicators.
-  </para>
-
-  <para>
-   The difference in NixOS is both these programs are patched to load plugins from a directory that is the value of an environment variable. All of which is controlled in Nix. If you need to configure the particular packages manually you can override the packages like:
-<programlisting>
-wingpanel-with-indicators.override {
-  indicators = [
-    pkgs.some-special-indicator
-  ];
-};
-
-switchboard-with-plugs.override {
-  plugs = [
-    pkgs.some-special-plug
-  ];
-};
-</programlisting>
-   please note that, like how the NixOS options describe these as extra plugins, this would only add to the default plugins included with the programs. If for some reason you'd like to configure which plugins to use exactly, both packages have an argument for this:
-<programlisting>
-wingpanel-with-indicators.override {
-  useDefaultIndicators = false;
-  indicators = specialListOfIndicators;
-};
-
-switchboard-with-plugs.override {
-  useDefaultPlugs = false;
-  plugs = specialListOfPlugs;
-};
-</programlisting>
-   this could be most useful for testing a particular plug-in in isolation.
-  </para>
- </section>
- <section xml:id="sec-pantheon-faq">
-  <title>FAQ</title>
-
-  <variablelist>
-   <varlistentry xml:id="sec-pantheon-faq-messed-up-theme">
-    <term>
-     I have switched from a different desktop and Pantheon’s theming looks messed up.
-    </term>
-    <listitem>
-     <para>
-      Open Switchboard and go to: <guilabel>Administration</guilabel> → <guilabel>About</guilabel> → <guilabel>Restore Default Settings</guilabel> → <guibutton>Restore Settings</guibutton>. This will reset any dconf settings to their Pantheon defaults. Note this could reset certain GNOME specific preferences if that desktop was used prior.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry xml:id="sec-pantheon-faq-gnome-and-pantheon">
-    <term>
-     I cannot enable both GNOME and Pantheon.
-    </term>
-    <listitem>
-     <para>
-      This is a known <link xlink:href="https://github.com/NixOS/nixpkgs/issues/64611">issue</link> and there is no known workaround.
-     </para>
-    </listitem>
-   </varlistentry>
-   <varlistentry xml:id="sec-pantheon-faq-appcenter">
-    <term>
-     Does AppCenter work, or is it available?
-    </term>
-    <listitem>
-     <para>
-      AppCenter has been available since 20.03. Starting from 21.11, the Flatpak backend should work so you can install some Flatpak applications using it. However, due to missing appstream metadata, the Packagekit backend does not function currently. See this <link xlink:href="https://github.com/NixOS/nixpkgs/issues/15932">issue</link>.
-     </para>
-     <para>
-      If you are using Pantheon, AppCenter should be installed by default if you have <link linkend="module-services-flatpak">Flatpak support</link> enabled. If you also wish to add the <literal>appcenter</literal> Flatpak remote:
-     </para>
-<screen>
-<prompt>$ </prompt>flatpak remote-add --if-not-exists appcenter https://flatpak.elementary.io/repo.flatpakrepo
-</screen>
-    </listitem>
-   </varlistentry>
-  </variablelist>
- </section>
-</chapter>
diff --git a/nixos/modules/services/x11/desktop-managers/phosh.nix b/nixos/modules/services/x11/desktop-managers/phosh.nix
index e889c0e34e7..3cfa6e044b7 100644
--- a/nixos/modules/services/x11/desktop-managers/phosh.nix
+++ b/nixos/modules/services/x11/desktop-managers/phosh.nix
@@ -173,7 +173,7 @@ in
     systemd.services.phosh = {
       wantedBy = [ "graphical.target" ];
       serviceConfig = {
-        ExecStart = "${cfg.package}/bin/phosh";
+        ExecStart = "${cfg.package}/bin/phosh-session";
         User = cfg.user;
         Group = cfg.group;
         PAMName = "login";
diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix
index 9fcb408c287..38f932ffb42 100644
--- a/nixos/modules/services/x11/desktop-managers/plasma5.nix
+++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix
@@ -28,51 +28,10 @@ let
 
   libsForQt5 = pkgs.plasma5Packages;
   inherit (libsForQt5) kdeGear kdeFrameworks plasma5;
-  inherit (pkgs) writeText;
   inherit (lib)
     getBin optionalString literalExpression
     mkRemovedOptionModule mkRenamedOptionModule
-    mkDefault mkIf mkMerge mkOption types;
-
-  ini = pkgs.formats.ini { };
-
-  gtkrc2 = writeText "gtkrc-2.0" ''
-    # Default GTK+ 2 config for NixOS Plasma 5
-    include "/run/current-system/sw/share/themes/Breeze/gtk-2.0/gtkrc"
-    style "user-font"
-    {
-      font_name="Sans Serif Regular"
-    }
-    widget_class "*" style "user-font"
-    gtk-font-name="Sans Serif Regular 10"
-    gtk-theme-name="Breeze"
-    gtk-icon-theme-name="breeze"
-    gtk-fallback-icon-theme="hicolor"
-    gtk-cursor-theme-name="breeze_cursors"
-    gtk-toolbar-style=GTK_TOOLBAR_ICONS
-    gtk-menu-images=1
-    gtk-button-images=1
-  '';
-
-  gtk3_settings = ini.generate "settings.ini" {
-    Settings = {
-      gtk-font-name = "Sans Serif Regular 10";
-      gtk-theme-name = "Breeze";
-      gtk-icon-theme-name = "breeze";
-      gtk-fallback-icon-theme = "hicolor";
-      gtk-cursor-theme-name = "breeze_cursors";
-      gtk-toolbar-style = "GTK_TOOLBAR_ICONS";
-      gtk-menu-images = 1;
-      gtk-button-images = 1;
-    };
-  };
-
-  kcminputrc = ini.generate "kcminputrc" {
-    Mouse = {
-      cursorTheme = "breeze_cursors";
-      cursorSize = 0;
-    };
-  };
+    mkDefault mkIf mkMerge mkOption mkPackageOptionMD types;
 
   activationScript = ''
     ${set_XDG_CONFIG_HOME}
@@ -119,128 +78,93 @@ let
     XDG_CONFIG_HOME=''${XDG_CONFIG_HOME:-$HOME/.config}
   '';
 
-  startplasma = ''
-    ${set_XDG_CONFIG_HOME}
-    mkdir -p "''${XDG_CONFIG_HOME}"
-  '' + optionalString config.hardware.pulseaudio.enable ''
-    # Load PulseAudio module for routing support.
-    # See also: http://colin.guthr.ie/2009/10/so-how-does-the-kde-pulseaudio-support-work-anyway/
-      ${getBin config.hardware.pulseaudio.package}/bin/pactl load-module module-device-manager "do_routing=1"
-  '' + ''
-    ${activationScript}
-
-    # Create default configurations if Plasma has never been started.
-    kdeglobals="''${XDG_CONFIG_HOME}/kdeglobals"
-    if ! [ -f "$kdeglobals" ]; then
-      kcminputrc="''${XDG_CONFIG_HOME}/kcminputrc"
-      if ! [ -f "$kcminputrc" ]; then
-          cat ${kcminputrc} >"$kcminputrc"
-      fi
-
-      gtkrc2="$HOME/.gtkrc-2.0"
-      if ! [ -f "$gtkrc2" ]; then
-          cat ${gtkrc2} >"$gtkrc2"
-      fi
-
-      gtk3_settings="''${XDG_CONFIG_HOME}/gtk-3.0/settings.ini"
-      if ! [ -f "$gtk3_settings" ]; then
-          mkdir -p "$(dirname "$gtk3_settings")"
-          cat ${gtk3_settings} >"$gtk3_settings"
-      fi
-    fi
-  '';
-
 in
 
 {
-  options.services.xserver.desktopManager.plasma5 = {
-    enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc "Enable the Plasma 5 (KDE 5) desktop environment.";
-    };
-
-    phononBackend = mkOption {
-      type = types.enum [ "gstreamer" "vlc" ];
-      default = "gstreamer";
-      example = "vlc";
-      description = lib.mdDoc "Phonon audio backend to install.";
-    };
-
-    supportDDC = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Support setting monitor brightness via DDC.
+  options = {
+    services.xserver.desktopManager.plasma5 = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc "Enable the Plasma 5 (KDE 5) desktop environment.";
+      };
 
-        This is not needed for controlling brightness of the internal monitor
-        of a laptop and as it is considered experimental by upstream, it is
-        disabled by default.
-      '';
-    };
+      phononBackend = mkOption {
+        type = types.enum [ "gstreamer" "vlc" ];
+        default = "vlc";
+        example = "gstreamer";
+        description = lib.mdDoc "Phonon audio backend to install.";
+      };
 
-    useQtScaling = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc "Enable HiDPI scaling in Qt.";
-    };
+      useQtScaling = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc "Enable HiDPI scaling in Qt.";
+      };
 
-    runUsingSystemd = mkOption {
-      description = lib.mdDoc "Use systemd to manage the Plasma session";
-      type = types.bool;
-      default = true;
-    };
+      runUsingSystemd = mkOption {
+        description = lib.mdDoc "Use systemd to manage the Plasma session";
+        type = types.bool;
+        default = true;
+      };
 
-    excludePackages = mkOption {
-      description = lib.mdDoc "List of default packages to exclude from the configuration";
-      type = types.listOf types.package;
-      default = [];
-      example = literalExpression "[ pkgs.plasma5Packages.oxygen ]";
-    };
+      notoPackage = mkPackageOptionMD pkgs "Noto fonts" {
+        default = [ "noto-fonts" ];
+        example = "noto-fonts-lgc-plus";
+      };
 
-    # Internally allows configuring kdeglobals globally
-    kdeglobals = mkOption {
-      internal = true;
-      default = {};
-      type = kdeConfigurationType;
-    };
+      # Internally allows configuring kdeglobals globally
+      kdeglobals = mkOption {
+        internal = true;
+        default = {};
+        type = kdeConfigurationType;
+      };
 
-    # Internally allows configuring kwin globally
-    kwinrc = mkOption {
-      internal = true;
-      default = {};
-      type = kdeConfigurationType;
-    };
+      # Internally allows configuring kwin globally
+      kwinrc = mkOption {
+        internal = true;
+        default = {};
+        type = kdeConfigurationType;
+      };
 
-    mobile.enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Enable support for running the Plasma Mobile shell.
-      '';
-    };
+      mobile.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Enable support for running the Plasma Mobile shell.
+        '';
+      };
 
-    mobile.installRecommendedSoftware = mkOption {
-      type = types.bool;
-      default = true;
-      description = lib.mdDoc ''
-        Installs software recommended for use with Plasma Mobile, but which
-        is not strictly required for Plasma Mobile to run.
-      '';
-    };
+      mobile.installRecommendedSoftware = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          Installs software recommended for use with Plasma Mobile, but which
+          is not strictly required for Plasma Mobile to run.
+        '';
+      };
 
-    bigscreen.enable = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Enable support for running the Plasma Bigscreen session.
-      '';
+      bigscreen.enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Enable support for running the Plasma Bigscreen session.
+        '';
+      };
     };
+    environment.plasma5.excludePackages = mkOption {
+        description = lib.mdDoc "List of default packages to exclude from the configuration";
+        type = types.listOf types.package;
+        default = [];
+        example = literalExpression "[ pkgs.plasma5Packages.oxygen ]";
+      };
   };
 
   imports = [
     (mkRemovedOptionModule [ "services" "xserver" "desktopManager" "plasma5" "enableQt4Support" ] "Phonon no longer supports Qt 4.")
+    (mkRemovedOptionModule [ "services" "xserver" "desktopManager" "plasma5" "supportDDC" ] "DDC/CI is no longer supported upstream.")
     (mkRenamedOptionModule [ "services" "xserver" "desktopManager" "kde5" ] [ "services" "xserver" "desktopManager" "plasma5" ])
+    (mkRenamedOptionModule [ "services" "xserver" "desktopManager" "plasma5" "excludePackages" ] [ "environment" "plasma5" "excludePackages" ])
   ];
 
   config = mkMerge [
@@ -268,12 +192,6 @@ in
         };
       };
 
-      # DDC support
-      boot.kernelModules = lib.optional cfg.supportDDC "i2c_dev";
-      services.udev.extraRules = lib.optionalString cfg.supportDDC ''
-        KERNEL=="i2c-[0-9]*", TAG+="uaccess"
-      '';
-
       environment.systemPackages =
         with libsForQt5;
         with plasma5; with kdeGear; with kdeFrameworks;
@@ -360,6 +278,7 @@ in
             pkgs.xdg-user-dirs # Update user dirs as described in https://freedesktop.org/wiki/Software/xdg-user-dirs/
           ];
           optionalPackages = [
+            pkgs.aha # needed by kinfocenter for fwupd support
             plasma-browser-integration
             konsole
             oxygen
@@ -367,7 +286,7 @@ in
           ];
         in
         requiredPackages
-        ++ utils.removePackagesByName optionalPackages cfg.excludePackages
+        ++ utils.removePackagesByName optionalPackages config.environment.plasma5.excludePackages
 
         # Phonon audio backend
         ++ lib.optional (cfg.phononBackend == "gstreamer") libsForQt5.phonon-backend-gstreamer
@@ -382,7 +301,8 @@ in
         ++ lib.optional config.services.colord.enable pkgs.colord-kde
         ++ lib.optional config.services.hardware.bolt.enable pkgs.plasma5Packages.plasma-thunderbolt
         ++ lib.optionals config.services.samba.enable [ kdenetwork-filesharing pkgs.samba ]
-        ++ lib.optional config.services.xserver.wacom.enable pkgs.wacomtablet;
+        ++ lib.optional config.services.xserver.wacom.enable pkgs.wacomtablet
+        ++ lib.optional config.services.flatpak.enable flatpak-kcm;
 
       # Extra services for D-Bus activation
       services.dbus.packages = [
@@ -396,12 +316,23 @@ in
 
       environment.etc."X11/xkb".source = xcfg.xkbDir;
 
-      environment.sessionVariables.PLASMA_USE_QT_SCALING = mkIf cfg.useQtScaling "1";
+      environment.sessionVariables = {
+        PLASMA_USE_QT_SCALING = mkIf cfg.useQtScaling "1";
+
+        # Needed for things that depend on other store.kde.org packages to install correctly,
+        # notably Plasma look-and-feel packages (a.k.a. Global Themes)
+        #
+        # FIXME: this is annoyingly impure and should really be fixed at source level somehow,
+        # but kpackage is a library so we can't just wrap the one thing invoking it and be done.
+        # This also means things won't work for people not on Plasma, but at least this way it
+        # works for SOME people.
+        KPACKAGE_DEP_RESOLVERS_PATH = "${pkgs.plasma5Packages.frameworkintegration.out}/libexec/kf5/kpackagehandlers";
+      };
 
       # Enable GTK applications to load SVG icons
       services.xserver.gdk-pixbuf.modulePackages = [ pkgs.librsvg ];
 
-      fonts.fonts = with pkgs; [ noto-fonts hack-font ];
+      fonts.fonts = with pkgs; [ cfg.notoPackage hack-font ];
       fonts.fontconfig.defaultFonts = {
         monospace = [ "Hack" "Noto Sans Mono" ];
         sansSerif = [ "Noto Sans" ];
@@ -433,12 +364,7 @@ in
 
       security.pam.services.kde = { allowNullPassword = true; };
 
-      # Doing these one by one seems silly, but we currently lack a better
-      # construct for handling common pam configs.
-      security.pam.services.gdm.enableKwallet = true;
-      security.pam.services.kdm.enableKwallet = true;
-      security.pam.services.lightdm.enableKwallet = true;
-      security.pam.services.sddm.enableKwallet = true;
+      security.pam.services.login.enableKwallet = true;
 
       systemd.user.services = {
         plasma-early-setup = mkIf cfg.runUsingSystemd {
@@ -457,7 +383,6 @@ in
 
       # Update the start menu for each user that is currently logged in
       system.userActivationScripts.plasmaSetup = activationScript;
-      services.xserver.displayManager.setupCommands = startplasma;
 
       nixpkgs.config.firefox.enablePlasmaBrowserIntegration = true;
     })
@@ -504,16 +429,19 @@ in
             dolphin-plugins
             ffmpegthumbs
             kdegraphics-thumbnailers
+            kde-inotify-survey
+            kio-admin
             kio-extras
           ];
           optionalPackages = [
+            ark
             elisa
             gwenview
             okular
             khelpcenter
             print-manager
           ];
-      in requiredPackages ++ utils.removePackagesByName optionalPackages cfg.excludePackages;
+      in requiredPackages ++ utils.removePackagesByName optionalPackages config.environment.plasma5.excludePackages;
 
       systemd.user.services = {
         plasma-run-with-systemd = {
@@ -545,7 +473,7 @@ in
         }
         {
           # The user interface breaks without pulse
-          assertion = config.hardware.pulseaudio.enable;
+          assertion = config.hardware.pulseaudio.enable || (config.services.pipewire.enable && config.services.pipewire.pulse.enable);
           message = "Plasma Mobile requires pulseaudio.";
         }
       ];
diff --git a/nixos/modules/services/x11/display-managers/gdm.nix b/nixos/modules/services/x11/display-managers/gdm.nix
index 1c3881bef2d..f8f82bda3fa 100644
--- a/nixos/modules/services/x11/display-managers/gdm.nix
+++ b/nixos/modules/services/x11/display-managers/gdm.nix
@@ -323,7 +323,7 @@ in
 
         account   sufficient    pam_unix.so
 
-        password  requisite     pam_unix.so nullok sha512
+        password  requisite     pam_unix.so nullok yescrypt
 
         session   optional      pam_keyinit.so revoke
         session   include       login
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index f74e8efb8f6..548d3c5bc46 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -32,7 +32,7 @@ let
   usersConf = writeText "users.conf"
     ''
       [UserList]
-      minimum-uid=500
+      minimum-uid=1000
       hidden-users=${concatStringsSep " " dmcfg.hiddenUsers}
       hidden-shells=/run/current-system/sw/bin/nologin
     '';
@@ -302,7 +302,7 @@ in
 
         account   sufficient    pam_unix.so
 
-        password  requisite     pam_unix.so nullok sha512
+        password  requisite     pam_unix.so nullok yescrypt
 
         session   optional      pam_keyinit.so revoke
         session   include       login
diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix
index a3f03d7a19a..0ddeac0f109 100644
--- a/nixos/modules/services/x11/display-managers/sddm.nix
+++ b/nixos/modules/services/x11/display-managers/sddm.nix
@@ -215,10 +215,12 @@ in
     };
 
     security.pam.services = {
-      sddm = {
-        allowNullPassword = true;
-        startSession = true;
-      };
+      sddm.text = ''
+        auth      substack      login
+        account   include       login
+        password  substack      login
+        session   include       login
+      '';
 
       sddm-greeter.text = ''
         auth     required       pam_succeed_if.so audit quiet_success user = sddm
diff --git a/nixos/modules/services/x11/extra-layouts.nix b/nixos/modules/services/x11/extra-layouts.nix
index 574657a50c8..1f48713a68d 100644
--- a/nixos/modules/services/x11/extra-layouts.nix
+++ b/nixos/modules/services/x11/extra-layouts.nix
@@ -106,9 +106,9 @@ in
       description = lib.mdDoc ''
         Extra custom layouts that will be included in the xkb configuration.
         Information on how to create a new layout can be found here:
-        [](https://www.x.org/releases/current/doc/xorg-docs/input/XKB-Enhancing.html#Defining_New_Layouts).
+        <https://www.x.org/releases/current/doc/xorg-docs/input/XKB-Enhancing.html#Defining_New_Layouts>.
         For more examples see
-        [](https://wiki.archlinux.org/index.php/X_KeyBoard_extension#Basic_examples)
+        <https://wiki.archlinux.org/index.php/X_KeyBoard_extension#Basic_examples>
       '';
     };
 
@@ -121,7 +121,7 @@ in
     environment.sessionVariables = {
       # runtime override supported by multiple libraries e. g. libxkbcommon
       # https://xkbcommon.org/doc/current/group__include-path.html
-      XKB_CONFIG_ROOT = "${xkb_patched}/etc/X11/xkb";
+      XKB_CONFIG_ROOT = config.services.xserver.xkbDir;
     };
 
     services.xserver = {
diff --git a/nixos/modules/services/x11/gdk-pixbuf.nix b/nixos/modules/services/x11/gdk-pixbuf.nix
index 2105224f92f..9c088e4cc42 100644
--- a/nixos/modules/services/x11/gdk-pixbuf.nix
+++ b/nixos/modules/services/x11/gdk-pixbuf.nix
@@ -21,7 +21,7 @@ in
   # loaders.cache based on that and set the environment variable
   # GDK_PIXBUF_MODULE_FILE to point to it.
   config = lib.mkIf (cfg.modulePackages != []) {
-    environment.variables = {
+    environment.sessionVariables = {
       GDK_PIXBUF_MODULE_FILE = "${loadersCache}";
     };
   };
diff --git a/nixos/modules/services/x11/picom.nix b/nixos/modules/services/x11/picom.nix
index 4a0578de09c..1d6f3daa402 100644
--- a/nixos/modules/services/x11/picom.nix
+++ b/nixos/modules/services/x11/picom.nix
@@ -41,7 +41,7 @@ let
 in {
 
   imports = [
-    (mkAliasOptionModule [ "services" "compton" ] [ "services" "picom" ])
+    (mkAliasOptionModuleMD [ "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.
diff --git a/nixos/modules/services/x11/window-managers/i3.nix b/nixos/modules/services/x11/window-managers/i3.nix
index 64109e0c39f..5bb73cd0bfb 100644
--- a/nixos/modules/services/x11/window-managers/i3.nix
+++ b/nixos/modules/services/x11/window-managers/i3.nix
@@ -31,7 +31,6 @@ in
       type        = types.package;
       default     = pkgs.i3;
       defaultText = literalExpression "pkgs.i3";
-      example     = literalExpression "pkgs.i3-gaps";
       description = lib.mdDoc ''
         i3 package to use.
       '';
@@ -73,6 +72,6 @@ in
 
   imports = [
     (mkRemovedOptionModule [ "services" "xserver" "windowManager" "i3-gaps" "enable" ]
-      "Use services.xserver.windowManager.i3.enable and set services.xserver.windowManager.i3.package to pkgs.i3-gaps to use i3-gaps.")
+      "i3-gaps was merged into i3. Use services.xserver.windowManager.i3.enable instead.")
   ];
 }
diff --git a/nixos/modules/services/x11/window-managers/katriawm.nix b/nixos/modules/services/x11/window-managers/katriawm.nix
index 106631792ff..9a3fd5f3ca4 100644
--- a/nixos/modules/services/x11/window-managers/katriawm.nix
+++ b/nixos/modules/services/x11/window-managers/katriawm.nix
@@ -1,7 +1,7 @@
 { config, lib, pkgs, ... }:
 
 let
-  inherit (lib) mdDoc mkEnableOption mkIf mkPackageOption singleton;
+  inherit (lib) mdDoc mkEnableOption mkIf mkPackageOptionMD singleton;
   cfg = config.services.xserver.windowManager.katriawm;
 in
 {
@@ -9,7 +9,7 @@ in
   options = {
     services.xserver.windowManager.katriawm = {
       enable = mkEnableOption (mdDoc "katriawm");
-      package = mkPackageOption pkgs "katriawm" {};
+      package = mkPackageOptionMD pkgs "katriawm" {};
     };
   };
 
diff --git a/nixos/modules/services/x11/window-managers/nimdow.nix b/nixos/modules/services/x11/window-managers/nimdow.nix
new file mode 100644
index 00000000000..de319287602
--- /dev/null
+++ b/nixos/modules/services/x11/window-managers/nimdow.nix
@@ -0,0 +1,23 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.xserver.windowManager.nimdow;
+in
+{
+  options = {
+    services.xserver.windowManager.nimdow.enable = mkEnableOption (lib.mdDoc "nimdow");
+  };
+
+  config = mkIf cfg.enable {
+    services.xserver.windowManager.session = singleton {
+      name = "nimdow";
+      start = ''
+        ${pkgs.nimdow}/bin/nimdow &
+        waitPID=$!
+      '';
+    };
+    environment.systemPackages = [ pkgs.nimdow ];
+  };
+}
diff --git a/nixos/modules/services/x11/window-managers/qtile.nix b/nixos/modules/services/x11/window-managers/qtile.nix
index 523642591d9..b07fd8a9042 100644
--- a/nixos/modules/services/x11/window-managers/qtile.nix
+++ b/nixos/modules/services/x11/window-managers/qtile.nix
@@ -1,23 +1,62 @@
-{ config, lib, pkgs, ... }:
+{ config, pkgs, lib, ... }:
 
 with lib;
 
 let
   cfg = config.services.xserver.windowManager.qtile;
+  pyEnv = pkgs.python3.withPackages (p: [ (cfg.package.unwrapped or cfg.package) ] ++ (cfg.extraPackages p));
 in
 
 {
   options.services.xserver.windowManager.qtile = {
     enable = mkEnableOption (lib.mdDoc "qtile");
 
-    package = mkPackageOption pkgs "qtile" { };
+    package = mkPackageOptionMD pkgs "qtile-unwrapped" { };
+
+    configFile = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = literalExpression "./your_config.py";
+      description = lib.mdDoc ''
+          Path to the qtile configuration file.
+          If null, $XDG_CONFIG_HOME/qtile/config.py will be used.
+      '';
+    };
+
+    backend = mkOption {
+      type = types.enum [ "x11" "wayland" ];
+      default = "x11";
+      description = lib.mdDoc ''
+          Backend to use in qtile: `x11` or `wayland`.
+      '';
+    };
+
+    extraPackages = mkOption {
+        type = types.functionTo (types.listOf types.package);
+        default = _: [];
+        defaultText = literalExpression ''
+          python3Packages: with python3Packages; [];
+        '';
+        description = lib.mdDoc ''
+          Extra Python packages available to Qtile.
+          An example would be to include `python3Packages.qtile-extras`
+          for additional unoffical widgets.
+        '';
+        example = literalExpression ''
+          python3Packages: with python3Packages; [
+            qtile-extras
+          ];
+        '';
+      };
   };
 
   config = mkIf cfg.enable {
     services.xserver.windowManager.session = [{
       name = "qtile";
       start = ''
-        ${cfg.package}/bin/qtile start &
+        ${pyEnv}/bin/qtile start -b ${cfg.backend} \
+        ${optionalString (cfg.configFile != null)
+        "--config \"${cfg.configFile}\""} &
         waitPID=$!
       '';
     }];
diff --git a/nixos/modules/services/x11/window-managers/stumpwm.nix b/nixos/modules/services/x11/window-managers/stumpwm.nix
index 162af689dbb..c6fc49f5821 100644
--- a/nixos/modules/services/x11/window-managers/stumpwm.nix
+++ b/nixos/modules/services/x11/window-managers/stumpwm.nix
@@ -15,10 +15,10 @@ in
     services.xserver.windowManager.session = singleton {
       name = "stumpwm";
       start = ''
-        ${pkgs.lispPackages.stumpwm}/bin/stumpwm &
+        ${pkgs.sbclPackages.stumpwm}/bin/stumpwm &
         waitPID=$!
       '';
     };
-    environment.systemPackages = [ pkgs.lispPackages.stumpwm ];
+    environment.systemPackages = [ pkgs.sbclPackages.stumpwm ];
   };
 }
diff --git a/nixos/modules/services/x11/xserver.nix b/nixos/modules/services/x11/xserver.nix
index 83a71dcf23e..c0051a2ce38 100644
--- a/nixos/modules/services/x11/xserver.nix
+++ b/nixos/modules/services/x11/xserver.nix
@@ -121,7 +121,7 @@ let
           fi
         done
 
-        for i in $(find ${toString cfg.modules} -type d); do
+        for i in $(find ${toString cfg.modules} -type d | sort); do
           if test $(echo $i/*.so* | wc -w) -ne 0; then
             echo "  ModulePath \"$i\"" >> $out
           fi
@@ -138,6 +138,26 @@ let
     concatMapStringsSep "\n" (line: prefix + line) (splitString "\n" str);
 
   indent = prefixStringLines "  ";
+
+  # A scalable variant of the X11 "core" cursor
+  #
+  # If not running a fancy desktop environment, the cursor is likely set to
+  # the default `cursor.pcf` bitmap font. This is 17px wide, so it's very
+  # small and almost invisible on 4K displays.
+  fontcursormisc_hidpi = pkgs.xorg.fontxfree86type1.overrideAttrs (old:
+    let
+      # The scaling constant is 230/96: the scalable `left_ptr` glyph at
+      # about 23 points is rendered as 17px, on a 96dpi display.
+      # Note: the XLFD font size is in decipoints.
+      size = 2.39583 * cfg.dpi;
+      sizeString = builtins.head (builtins.split "\\." (toString size));
+    in
+    {
+      postInstall = ''
+        alias='cursor -xfree86-cursor-medium-r-normal--0-${sizeString}-0-0-p-0-adobe-fontspecific'
+        echo "$alias" > $out/lib/X11/fonts/Type1/fonts.alias
+      '';
+    });
 in
 
 {
@@ -256,7 +276,7 @@ in
 
       videoDrivers = mkOption {
         type = types.listOf types.str;
-        default = [ "amdgpu" "radeon" "nouveau" "modesetting" "fbdev" ];
+        default = [ "modesetting" "fbdev" ];
         example = [
           "nvidia" "nvidiaLegacy390" "nvidiaLegacy340" "nvidiaLegacy304"
           "amdgpu-pro"
@@ -576,6 +596,15 @@ in
           Whether to terminate X upon server reset.
         '';
       };
+
+      upscaleDefaultCursor = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Upscale the default X cursor to be more visible on high-density displays.
+          Requires `config.services.xserver.dpi` to be set.
+        '';
+      };
     };
 
   };
@@ -592,7 +621,8 @@ in
                     || dmConf.sddm.enable
                     || dmConf.xpra.enable
                     || dmConf.sx.enable
-                    || dmConf.startx.enable);
+                    || dmConf.startx.enable
+                    || config.services.greetd.enable);
       in mkIf (default) (mkDefault true);
 
     # so that the service won't be enabled when only startx is used
@@ -626,6 +656,10 @@ in
                 + "${toString (length primaryHeads)} heads set to primary: "
                 + concatMapStringsSep ", " (x: x.output) primaryHeads;
       })
+      {
+        assertion = cfg.upscaleDefaultCursor -> cfg.dpi != null;
+        message = "Specify `config.services.xserver.dpi` to upscale the default cursor.";
+      }
     ];
 
     environment.etc =
@@ -850,6 +884,10 @@ in
       '';
 
     fonts.enableDefaultFonts = mkDefault true;
+    fonts.fonts = [
+      (if cfg.upscaleDefaultCursor then fontcursormisc_hidpi else pkgs.xorg.fontcursormisc)
+      pkgs.xorg.fontmiscmisc
+    ];
 
   };
 
diff --git a/nixos/modules/system/activation/bootspec.cue b/nixos/modules/system/activation/bootspec.cue
index 3fc9ca381df..1f7b4afa87a 100644
--- a/nixos/modules/system/activation/bootspec.cue
+++ b/nixos/modules/system/activation/bootspec.cue
@@ -1,4 +1,7 @@
-#V1: {
+import "struct"
+
+#BootspecV1: {
+	system:         string
 	init:           string
 	initrd?:        string
 	initrdSecrets?: string
@@ -6,12 +9,23 @@
 	kernelParams: [...string]
 	label:    string
 	toplevel: string
-	specialisation?: {
-		[=~"^"]: #V1
-	}
-	extensions?: {...}
 }
 
-Document: {
-	v1: #V1
+// A restricted document does not allow any official specialisation
+// information in it to avoid "recursive specialisations".
+#RestrictedDocument: struct.MinFields(1) & {
+	"org.nixos.bootspec.v1": #BootspecV1
+	[=~"^"]:                 #BootspecExtension
+}
+
+// Specialisations are a hashmap of strings
+#BootspecSpecialisationV1: [string]: #RestrictedDocument
+
+// Bootspec extensions are defined by the extension author.
+#BootspecExtension: {...}
+
+// A "full" document allows official specialisation information
+// in the top-level with a reserved namespaced key.
+Document: #RestrictedDocument & {
+	"org.nixos.specialisation.v1"?: #BootspecSpecialisationV1
 }
diff --git a/nixos/modules/system/activation/bootspec.nix b/nixos/modules/system/activation/bootspec.nix
index da76bf9084a..9e1fa309d5d 100644
--- a/nixos/modules/system/activation/bootspec.nix
+++ b/nixos/modules/system/activation/bootspec.nix
@@ -16,18 +16,20 @@ let
       filename = "boot.json";
       json =
         pkgs.writeText filename
-          (builtins.toJSON
+        (builtins.toJSON
+          # Merge extensions first to not let them shadow NixOS bootspec data.
+          (cfg.extensions //
           {
-            v1 = {
+            "org.nixos.bootspec.v1" = {
+              system = config.boot.kernelPackages.stdenv.hostPlatform.system;
               kernel = "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}";
               kernelParams = config.boot.kernelParams;
+              label = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} (Linux ${config.boot.kernelPackages.kernel.modDirVersion})";
+            } // lib.optionalAttrs config.boot.initrd.enable {
               initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}";
               initrdSecrets = "${config.system.build.initialRamdiskSecretAppender}/bin/append-initrd-secrets";
-              label = "NixOS ${config.system.nixos.codeName} ${config.system.nixos.label} (Linux ${config.boot.kernelPackages.kernel.modDirVersion})";
-
-              inherit (cfg) extensions;
             };
-          });
+          }));
 
       generator =
         let
@@ -38,10 +40,10 @@ let
           # This can only be done here because we *cannot* depend on $out
           # referring to the toplevel, except by living in the toplevel itself.
           toplevelInjector = lib.escapeShellArgs [
-            "${pkgs.jq}/bin/jq"
+            "${pkgs.buildPackages.jq}/bin/jq"
             ''
-              .v1.toplevel = $toplevel |
-              .v1.init = $init
+              ."org.nixos.bootspec.v1".toplevel = $toplevel |
+              ."org.nixos.bootspec.v1".init = $init
             ''
             "--sort-keys"
             "--arg" "toplevel" "${placeholder "out"}"
@@ -54,20 +56,16 @@ let
           specialisationInjector =
             let
               specialisationLoader = (lib.mapAttrsToList
-                (childName: childToplevel: lib.escapeShellArgs [ "--slurpfile" childName "${childToplevel}/bootspec/${filename}" ])
+                (childName: childToplevel: lib.escapeShellArgs [ "--slurpfile" childName "${childToplevel}/${filename}" ])
                 children);
             in
             lib.escapeShellArgs [
-              "${pkgs.jq}/bin/jq"
+              "${pkgs.buildPackages.jq}/bin/jq"
               "--sort-keys"
-              ".v1.specialisation = ($ARGS.named | map_values(. | first | .v1))"
+              ''."org.nixos.specialisation.v1" = ($ARGS.named | map_values(. | first))''
             ] + " ${lib.concatStringsSep " " specialisationLoader}";
         in
-        ''
-          mkdir -p $out/bootspec
-
-          ${toplevelInjector} | ${specialisationInjector} > $out/bootspec/${filename}
-        '';
+        "${toplevelInjector} | ${specialisationInjector} > $out/${filename}";
 
       validator = pkgs.writeCueValidator ./bootspec.cue {
         document = "Document"; # Universal validator for any version as long the schema is correctly set.
@@ -77,10 +75,17 @@ let
 in
 {
   options.boot.bootspec = {
-    enable = lib.mkEnableOption (lib.mdDoc "Enable generation of RFC-0125 bootspec in $system/bootspec, e.g. /run/current-system/bootspec");
+    enable = lib.mkEnableOption (lib.mdDoc "the generation of RFC-0125 bootspec in $system/boot.json, e.g. /run/current-system/boot.json")
+      // { default = true; internal = true; };
+    enableValidation = lib.mkEnableOption (lib.mdDoc ''the validation of bootspec documents for each build.
+      This will introduce Go in the build-time closure as we are relying on [Cuelang](https://cuelang.org/) for schema validation.
+      Enable this option if you want to ascertain that your documents are correct.
+      ''
+    );
 
     extensions = lib.mkOption {
-      type = lib.types.attrs;
+      # NOTE(RaitoBezarius): this is not enough to validate: extensions."osRelease" = drv; those are picked up by cue validation.
+      type = lib.types.attrsOf lib.types.anything; # <namespace>: { ...namespace-specific fields }
       default = { };
       description = lib.mdDoc ''
         User-defined data that extends the bootspec document.
@@ -110,15 +115,4 @@ in
       default = schemas.v1.filename;
     };
   };
-
-  config = lib.mkIf (cfg.enable) {
-    warnings = [
-      ''RFC-0125 is not merged yet, this is a feature preview of bootspec.
-        The schema is not definitive and features are not guaranteed to be stable until RFC-0125 is merged.
-        See:
-        - https://github.com/NixOS/nixpkgs/pull/172237 to track merge status in nixpkgs.
-        - https://github.com/NixOS/rfcs/pull/125 to track RFC status.
-      ''
-    ];
-  };
 }
diff --git a/nixos/modules/system/activation/switch-to-configuration.pl b/nixos/modules/system/activation/switch-to-configuration.pl
index 9a4c635402d..de6e43dd30d 100755
--- a/nixos/modules/system/activation/switch-to-configuration.pl
+++ b/nixos/modules/system/activation/switch-to-configuration.pl
@@ -84,7 +84,7 @@ EOF
 
 # This is a NixOS installation if it has /etc/NIXOS or a proper
 # /etc/os-release.
-if (!-f "/etc/NIXOS" && (read_file("/etc/os-release", err_mode => "quiet") // "") !~ /^ID="?nixos"?/msx) {
+if (!-f "/etc/NIXOS" && (read_file("/etc/os-release", err_mode => "quiet") // "") !~ /^ID="?@distroId@"?/msx) {
     die("This is not a NixOS installation!\n");
 }
 
diff --git a/nixos/modules/system/activation/top-level.nix b/nixos/modules/system/activation/top-level.nix
index 0bb3628ceed..f2e74135478 100644
--- a/nixos/modules/system/activation/top-level.nix
+++ b/nixos/modules/system/activation/top-level.nix
@@ -65,6 +65,7 @@ let
 
       mkdir $out/bin
       export localeArchive="${config.i18n.glibcLocales}/lib/locale/locale-archive"
+      export distroId=${config.system.nixos.distroId};
       substituteAll ${./switch-to-configuration.pl} $out/bin/switch-to-configuration
       chmod +x $out/bin/switch-to-configuration
       ${optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
@@ -81,7 +82,8 @@ let
 
       ${optionalString (!config.boot.isContainer && config.boot.bootspec.enable) ''
         ${config.boot.bootspec.writer}
-        ${config.boot.bootspec.validator} "$out/bootspec/${config.boot.bootspec.filename}"
+        ${optionalString config.boot.bootspec.enableValidation
+          ''${config.boot.bootspec.validator} "$out/${config.boot.bootspec.filename}"''}
       ''}
 
       ${config.system.extraSystemBuilderCmds}
@@ -129,6 +131,13 @@ let
       pkgs.replaceDependency { inherit oldDependency newDependency drv; }
     ) baseSystemAssertWarn config.system.replaceRuntimeDependencies;
 
+  systemWithBuildDeps = system.overrideAttrs (o: {
+    systemBuildClosure = pkgs.closureInfo { rootPaths = [ system.drvPath ]; };
+    buildCommand = o.buildCommand + ''
+      ln -sn $systemBuildClosure $out/build-closure
+    '';
+  });
+
 in
 
 {
@@ -305,10 +314,37 @@ in
       '';
     };
 
+    system.includeBuildDependencies = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Whether to include the build closure of the whole system in
+        its runtime closure.  This can be useful for making changes
+        fully offline, as it includes all sources, patches, and
+        intermediate outputs required to build all the derivations
+        that the system depends on.
+
+        Note that this includes _all_ the derivations, down from the
+        included applications to their sources, the compilers used to
+        build them, and even the bootstrap compiler used to compile
+        the compilers. This increases the size of the system and the
+        time needed to download its dependencies drastically: a
+        minimal configuration with no extra services enabled grows
+        from ~670MiB in size to 13.5GiB, and takes proportionally
+        longer to download.
+      '';
+    };
+
   };
 
 
   config = {
+    assertions = [
+      {
+        assertion = config.system.copySystemConfiguration -> !lib.inPureEvalMode;
+        message = "system.copySystemConfiguration is not supported with flakes";
+      }
+    ];
 
     system.extraSystemBuilderCmds =
       optionalString
@@ -335,7 +371,7 @@ in
       ]; };
     };
 
-    system.build.toplevel = system;
+    system.build.toplevel = if config.system.includeBuildDependencies then systemWithBuildDeps else system;
 
   };
 
diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix
index 87e66f73be0..b003d983d2b 100644
--- a/nixos/modules/system/boot/binfmt.nix
+++ b/nixos/modules/system/boot/binfmt.nix
@@ -1,6 +1,6 @@
 { config, lib, pkgs, ... }:
 let
-  inherit (lib) mkOption types optionalString stringAfter;
+  inherit (lib) mkOption mkDefault types optionalString stringAfter;
 
   cfg = config.boot.binfmt;
 
@@ -125,6 +125,10 @@ let
       magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00'';
       mask = ''\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
     };
+    loongarch64-linux = {
+      magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x02\x01'';
+      mask = ''\xff\xff\xff\xff\xff\xff\xff\xfc\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
+    };
     wasm32-wasi = {
       magicOrExtension = ''\x00asm'';
       mask = ''\xff\xff\xff\xff'';
@@ -134,11 +138,11 @@ let
       mask = ''\xff\xff\xff\xff'';
     };
     x86_64-windows = {
-      magicOrExtension = ".exe";
+      magicOrExtension = "exe";
       recognitionType = "extension";
     };
     i686-windows = {
-      magicOrExtension = ".exe";
+      magicOrExtension = "exe";
       recognitionType = "extension";
     };
   };
@@ -281,7 +285,7 @@ in {
   config = {
     boot.binfmt.registrations = builtins.listToAttrs (map (system: {
       name = system;
-      value = let
+      value = { config, ... }: let
         interpreter = getEmulator system;
         qemuArch = getQemuArch system;
 
@@ -292,13 +296,13 @@ in {
         in
           if preserveArgvZero then "${wrapper}/bin/${wrapperName}"
           else interpreter;
-      in {
-        inherit preserveArgvZero;
+      in ({
+        preserveArgvZero = mkDefault preserveArgvZero;
 
-        interpreter = interpreterReg;
-        wrapInterpreterInShell = !preserveArgvZero;
-        interpreterSandboxPath = dirOf (dirOf interpreterReg);
-      } // (magics.${system} or (throw "Cannot create binfmt registration for system ${system}"));
+        interpreter = mkDefault interpreterReg;
+        wrapInterpreterInShell = mkDefault (!config.preserveArgvZero);
+        interpreterSandboxPath = mkDefault (dirOf (dirOf config.interpreter));
+      } // (magics.${system} or (throw "Cannot create binfmt registration for system ${system}")));
     }) cfg.emulatedSystems);
     nix.settings = lib.mkIf (cfg.emulatedSystems != []) {
       extra-platforms = cfg.emulatedSystems ++ lib.optional pkgs.stdenv.hostPlatform.isx86_64 "i686-linux";
@@ -316,11 +320,13 @@ in {
       mkdir -p -m 0755 /run/binfmt
       ${lib.concatStringsSep "\n" (lib.mapAttrsToList activationSnippet config.boot.binfmt.registrations)}
     '';
-    systemd.additionalUpstreamSystemUnits = lib.mkIf (config.boot.binfmt.registrations != {}) [
-      "proc-sys-fs-binfmt_misc.automount"
-      "proc-sys-fs-binfmt_misc.mount"
-      "systemd-binfmt.service"
-    ];
-    systemd.services.systemd-binfmt.restartTriggers = [ (builtins.toJSON config.boot.binfmt.registrations) ];
+    systemd = lib.mkIf (config.boot.binfmt.registrations != {}) {
+      additionalUpstreamSystemUnits = [
+        "proc-sys-fs-binfmt_misc.automount"
+        "proc-sys-fs-binfmt_misc.mount"
+        "systemd-binfmt.service"
+      ];
+      services.systemd-binfmt.restartTriggers = [ (builtins.toJSON config.boot.binfmt.registrations) ];
+    };
   };
 }
diff --git a/nixos/modules/system/boot/grow-partition.nix b/nixos/modules/system/boot/grow-partition.nix
index 034b2b9906f..a2764187a53 100644
--- a/nixos/modules/system/boot/grow-partition.nix
+++ b/nixos/modules/system/boot/grow-partition.nix
@@ -17,6 +17,11 @@ with lib;
 
   config = mkIf config.boot.growPartition {
 
+    assertions = [{
+      assertion = !config.boot.initrd.systemd.enable;
+      message = "systemd stage 1 does not support 'boot.growPartition' yet.";
+    }];
+
     boot.initrd.extraUtilsCommands = ''
       copy_bin_and_libs ${pkgs.gawk}/bin/gawk
       copy_bin_and_libs ${pkgs.gnused}/bin/sed
diff --git a/nixos/modules/system/boot/initrd-network.nix b/nixos/modules/system/boot/initrd-network.nix
index a1017c3e242..e8bbf1d0403 100644
--- a/nixos/modules/system/boot/initrd-network.nix
+++ b/nixos/modules/system/boot/initrd-network.nix
@@ -67,11 +67,15 @@ in
 
     boot.initrd.network.flushBeforeStage2 = mkOption {
       type = types.bool;
-      default = true;
+      default = !config.boot.initrd.systemd.enable;
+      defaultText = "!config.boot.initrd.systemd.enable";
       description = lib.mdDoc ''
         Whether to clear the configuration of the interfaces that were set up in
         the initrd right before stage 2 takes over. Stage 2 will do the regular network
         configuration based on the NixOS networking options.
+
+        The default is false when systemd is enabled in initrd,
+        because the systemd-networkd documentation suggests it.
       '';
     };
 
diff --git a/nixos/modules/system/boot/initrd-openvpn.nix b/nixos/modules/system/boot/initrd-openvpn.nix
index b41e7524320..2530240628e 100644
--- a/nixos/modules/system/boot/initrd-openvpn.nix
+++ b/nixos/modules/system/boot/initrd-openvpn.nix
@@ -51,7 +51,7 @@ in
 
     # Add openvpn and ip binaries to the initrd
     # The shared libraries are required for DNS resolution
-    boot.initrd.extraUtilsCommands = ''
+    boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
       copy_bin_and_libs ${pkgs.openvpn}/bin/openvpn
       copy_bin_and_libs ${pkgs.iproute2}/bin/ip
 
@@ -59,21 +59,33 @@ in
       cp -pv ${pkgs.glibc}/lib/libnss_dns.so.2 $out/lib
     '';
 
+    boot.initrd.systemd.storePaths = [
+      "${pkgs.openvpn}/bin/openvpn"
+      "${pkgs.iproute2}/bin/ip"
+      "${pkgs.glibc}/lib/libresolv.so.2"
+      "${pkgs.glibc}/lib/libnss_dns.so.2"
+    ];
+
     boot.initrd.secrets = {
       "/etc/initrd.ovpn" = cfg.configuration;
     };
 
     # openvpn --version would exit with 1 instead of 0
-    boot.initrd.extraUtilsCommandsTest = ''
+    boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
       $out/bin/openvpn --show-gateway
     '';
 
-    # Add `iproute /bin/ip` to the config, to ensure that openvpn
-    # is able to set the routes
-    boot.initrd.network.postCommands = ''
-      (cat /etc/initrd.ovpn; echo -e '\niproute /bin/ip') | \
-        openvpn /dev/stdin &
+    boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) ''
+      openvpn /etc/initrd.ovpn &
     '';
+
+    boot.initrd.systemd.services.openvpn = {
+      wantedBy = [ "initrd.target" ];
+      path = [ pkgs.iproute2 ];
+      after = [ "network.target" "initrd-nixos-copy-secrets.service" ];
+      serviceConfig.ExecStart = "${pkgs.openvpn}/bin/openvpn /etc/initrd.ovpn";
+      serviceConfig.Type = "notify";
+    };
   };
 
 }
diff --git a/nixos/modules/system/boot/initrd-ssh.nix b/nixos/modules/system/boot/initrd-ssh.nix
index 701d242abc1..60c5ff62fff 100644
--- a/nixos/modules/system/boot/initrd-ssh.nix
+++ b/nixos/modules/system/boot/initrd-ssh.nix
@@ -5,6 +5,10 @@ with lib;
 let
 
   cfg = config.boot.initrd.network.ssh;
+  shell = if cfg.shell == null then "/bin/ash" else cfg.shell;
+  inherit (config.programs.ssh) package;
+
+  enabled = let initrd = config.boot.initrd; in (initrd.network.enable || initrd.systemd.network.enable) && cfg.enable;
 
 in
 
@@ -33,8 +37,9 @@ in
     };
 
     shell = mkOption {
-      type = types.str;
-      default = "/bin/ash";
+      type = types.nullOr types.str;
+      default = null;
+      defaultText = ''"/bin/ash"'';
       description = lib.mdDoc ''
         Login shell of the remote user. Can be used to limit actions user can do.
       '';
@@ -119,22 +124,24 @@ in
     sshdCfg = config.services.openssh;
 
     sshdConfig = ''
+      UsePAM no
       Port ${toString cfg.port}
 
       PasswordAuthentication no
+      AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys2 /etc/ssh/authorized_keys.d/%u
       ChallengeResponseAuthentication no
 
       ${flip concatMapStrings cfg.hostKeys (path: ''
         HostKey ${initrdKeyPath path}
       '')}
 
-      KexAlgorithms ${concatStringsSep "," sshdCfg.kexAlgorithms}
-      Ciphers ${concatStringsSep "," sshdCfg.ciphers}
-      MACs ${concatStringsSep "," sshdCfg.macs}
+      KexAlgorithms ${concatStringsSep "," sshdCfg.settings.KexAlgorithms}
+      Ciphers ${concatStringsSep "," sshdCfg.settings.Ciphers}
+      MACs ${concatStringsSep "," sshdCfg.settings.Macs}
 
-      LogLevel ${sshdCfg.logLevel}
+      LogLevel ${sshdCfg.settings.LogLevel}
 
-      ${if sshdCfg.useDns then ''
+      ${if sshdCfg.settings.UseDns then ''
         UseDNS yes
       '' else ''
         UseDNS no
@@ -142,7 +149,7 @@ in
 
       ${cfg.extraConfig}
     '';
-  in mkIf (config.boot.initrd.network.enable && cfg.enable) {
+  in mkIf enabled {
     assertions = [
       {
         assertion = cfg.authorizedKeys != [];
@@ -157,14 +164,19 @@ in
           for instructions.
         '';
       }
+
+      {
+        assertion = config.boot.initrd.systemd.enable -> cfg.shell == null;
+        message = "systemd stage 1 does not support boot.initrd.network.ssh.shell";
+      }
     ];
 
-    boot.initrd.extraUtilsCommands = ''
-      copy_bin_and_libs ${pkgs.openssh}/bin/sshd
+    boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
+      copy_bin_and_libs ${package}/bin/sshd
       cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib
     '';
 
-    boot.initrd.extraUtilsCommandsTest = ''
+    boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
       # sshd requires a host key to check config, so we pass in the test's
       tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)"
       cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey"
@@ -176,9 +188,9 @@ in
       rm "$tmpkey"
     '';
 
-    boot.initrd.network.postCommands = ''
-      echo '${cfg.shell}' > /etc/shells
-      echo 'root:x:0:0:root:/root:${cfg.shell}' > /etc/passwd
+    boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) ''
+      echo '${shell}' > /etc/shells
+      echo 'root:x:0:0:root:/root:${shell}' > /etc/passwd
       echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd
       echo 'passwd: files' > /etc/nsswitch.conf
 
@@ -204,7 +216,7 @@ in
       /bin/sshd -e
     '';
 
-    boot.initrd.postMountCommands = ''
+    boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) ''
       # Stop sshd cleanly before stage 2.
       #
       # If you want to keep it around to debug post-mount SSH issues,
@@ -217,6 +229,38 @@ in
 
     boot.initrd.secrets = listToAttrs
       (map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys);
+
+    # Systemd initrd stuff
+    boot.initrd.systemd = mkIf config.boot.initrd.systemd.enable {
+      users.sshd = { uid = 1; group = "sshd"; };
+      groups.sshd = { gid = 1; };
+
+      contents."/etc/ssh/authorized_keys.d/root".text =
+        concatStringsSep "\n" config.boot.initrd.network.ssh.authorizedKeys;
+      contents."/etc/ssh/sshd_config".text = sshdConfig;
+      storePaths = ["${package}/bin/sshd"];
+
+      services.sshd = {
+        description = "SSH Daemon";
+        wantedBy = ["initrd.target"];
+        after = ["network.target" "initrd-nixos-copy-secrets.service"];
+
+        # Keys from Nix store are world-readable, which sshd doesn't
+        # like. If this were a real nix store and not the initrd, we
+        # neither would nor could do this
+        preStart = flip concatMapStrings cfg.hostKeys (path: ''
+          /bin/chmod 0600 "${initrdKeyPath path}"
+        '');
+        unitConfig.DefaultDependencies = false;
+        serviceConfig = {
+          ExecStart = "${package}/bin/sshd -D -f /etc/ssh/sshd_config";
+          Type = "simple";
+          KillMode = "process";
+          Restart = "on-failure";
+        };
+      };
+    };
+
   };
 
 }
diff --git a/nixos/modules/system/boot/kernel.nix b/nixos/modules/system/boot/kernel.nix
index b13e50cb17d..0298e28f328 100644
--- a/nixos/modules/system/boot/kernel.nix
+++ b/nixos/modules/system/boot/kernel.nix
@@ -20,7 +20,7 @@ in
   ###### interface
 
   options = {
-    boot.kernel.enable = mkEnableOption (lib.mdDoc "the Linux kernel. This is useful for systemd-like containers which do not require a kernel.") // {
+    boot.kernel.enable = mkEnableOption (lib.mdDoc "the Linux kernel. This is useful for systemd-like containers which do not require a kernel") // {
       default = true;
     };
 
@@ -73,8 +73,46 @@ in
     boot.kernelPatches = mkOption {
       type = types.listOf types.attrs;
       default = [];
-      example = literalExpression "[ pkgs.kernelPatches.ubuntu_fan_4_4 ]";
-      description = lib.mdDoc "A list of additional patches to apply to the kernel.";
+      example = literalExpression ''
+        [
+          {
+            name = "foo";
+            patch = ./foo.patch;
+            extraStructuredConfig.FOO = lib.kernel.yes;
+            features.foo = true;
+          }
+        ]
+      '';
+      description = lib.mdDoc ''
+        A list of additional patches to apply to the kernel.
+
+        Every item should be an attribute set with the following attributes:
+
+        ```nix
+        {
+          name = "foo";                 # descriptive name, required
+
+          patch = ./foo.patch;          # path or derivation that contains the patch source
+                                        # (required, but can be null if only config changes
+                                        # are needed)
+
+          extraStructuredConfig = {     # attrset of extra configuration parameters
+            FOO = lib.kernel.yes;       # (without the CONFIG_ prefix, optional)
+          };                            # values should generally be lib.kernel.yes,
+                                        # lib.kernel.no or lib.kernel.module
+
+          features = {                  # attrset of extra "features" the kernel is considered to have
+            foo = true;                 # (may be checked by other NixOS modules, optional)
+          };
+
+          extraConfig = "CONFIG_FOO y"; # extra configuration options in string form
+                                        # (deprecated, use extraStructuredConfig instead, optional)
+        }
+        ```
+
+        There's a small set of existing kernel patches in Nixpkgs, available as `pkgs.kernelPatches`,
+        that follow this format and can be used directly.
+      '';
     };
 
     boot.kernel.randstructSeed = mkOption {
diff --git a/nixos/modules/system/boot/loader/external/external.md b/nixos/modules/system/boot/loader/external/external.md
index ba1dfd4d9b9..4f5b559dfc4 100644
--- a/nixos/modules/system/boot/loader/external/external.md
+++ b/nixos/modules/system/boot/loader/external/external.md
@@ -20,7 +20,7 @@ You can enable FooBoot like this:
 }
 ```
 
-## Developing Custom Bootloader Backends
+## Developing Custom Bootloader Backends {#sec-bootloader-external-developing}
 
 Bootloaders should use [RFC-0125](https://github.com/NixOS/rfcs/pull/125)'s Bootspec format and synthesis tools to identify the key properties for bootable system generations.
 
diff --git a/nixos/modules/system/boot/loader/external/external.nix b/nixos/modules/system/boot/loader/external/external.nix
index 5cf478e6c83..926cbd2b4b3 100644
--- a/nixos/modules/system/boot/loader/external/external.nix
+++ b/nixos/modules/system/boot/loader/external/external.nix
@@ -8,9 +8,7 @@ in
 {
   meta = {
     maintainers = with maintainers; [ cole-h grahamc raitobezarius ];
-    # Don't edit the docbook xml directly, edit the md and generate it:
-    # `pandoc external.md -t docbook --top-level-division=chapter --extract-media=media -f markdown+smart > external.xml`
-    doc = ./external.xml;
+    doc = ./external.md;
   };
 
   options.boot.loader.external = {
diff --git a/nixos/modules/system/boot/loader/external/external.xml b/nixos/modules/system/boot/loader/external/external.xml
deleted file mode 100644
index 39ab2156bc8..00000000000
--- a/nixos/modules/system/boot/loader/external/external.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<chapter xmlns="http://docbook.org/ns/docbook" xmlns:xlink="http://www.w3.org/1999/xlink" xml:id="sec-bootloader-external">
-  <title>External Bootloader Backends</title>
-  <para>
-    NixOS has support for several bootloader backends by default:
-    systemd-boot, grub, uboot, etc. The built-in bootloader backend
-    support is generic and supports most use cases. Some users may
-    prefer to create advanced workflows around managing the bootloader
-    and bootable entries.
-  </para>
-  <para>
-    You can replace the built-in bootloader support with your own
-    tooling using the <quote>external</quote> bootloader option.
-  </para>
-  <para>
-    Imagine you have created a new package called FooBoot. FooBoot
-    provides a program at
-    <literal>${pkgs.fooboot}/bin/fooboot-install</literal> which takes
-    the system closure’s path as its only argument and configures the
-    system’s bootloader.
-  </para>
-  <para>
-    You can enable FooBoot like this:
-  </para>
-  <programlisting language="nix">
-{ pkgs, ... }: {
-  boot.loader.external = {
-    enable = true;
-    installHook = &quot;${pkgs.fooboot}/bin/fooboot-install&quot;;
-  };
-}
-</programlisting>
-  <section xml:id="developing-custom-bootloader-backends">
-    <title>Developing Custom Bootloader Backends</title>
-    <para>
-      Bootloaders should use
-      <link xlink:href="https://github.com/NixOS/rfcs/pull/125">RFC-0125</link>’s
-      Bootspec format and synthesis tools to identify the key properties
-      for bootable system generations.
-    </para>
-  </section>
-</chapter>
diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix
index 1d266b5a37d..5c0a07fb512 100644
--- a/nixos/modules/system/boot/loader/grub/grub.nix
+++ b/nixos/modules/system/boot/loader/grub/grub.nix
@@ -33,7 +33,7 @@ let
     then realGrub.override { efiSupport = cfg.efiSupport; }
     else null;
 
-  f = x: if x == null then "" else "" + x;
+  f = x: optionalString (x != null) ("" + x);
 
   grubConfig = args:
     let
@@ -52,10 +52,10 @@ let
       fullName = lib.getName realGrub;
       fullVersion = lib.getVersion realGrub;
       grubEfi = f grubEfi;
-      grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f (grubEfi.grubTarget or "") else "";
+      grubTargetEfi = optionalString (cfg.efiSupport && (cfg.version == 2)) (f (grubEfi.grubTarget or ""));
       bootPath = args.path;
       storePath = config.boot.loader.grub.storePath;
-      bootloaderId = if args.efiBootloaderId == null then "NixOS${efiSysMountPoint'}" else args.efiBootloaderId;
+      bootloaderId = if args.efiBootloaderId == null then "${config.system.nixos.distroName}${efiSysMountPoint'}" else args.efiBootloaderId;
       timeout = if config.boot.loader.timeout == null then -1 else config.boot.loader.timeout;
       users = if cfg.users == {} || cfg.version != 1 then cfg.users else throw "GRUB version 1 does not support user accounts.";
       theme = f cfg.theme;
@@ -759,6 +759,7 @@ in
             src = ./install-grub.pl;
             utillinux = pkgs.util-linux;
             btrfsprogs = pkgs.btrfs-progs;
+            inherit (config.system.nixos) distroName;
           };
           perl = pkgs.perl.withPackages (p: with p; [
             FileSlurp FileCopyRecursive
diff --git a/nixos/modules/system/boot/loader/grub/install-grub.pl b/nixos/modules/system/boot/loader/grub/install-grub.pl
index d5f019423b6..2779f26aa1b 100644
--- a/nixos/modules/system/boot/loader/grub/install-grub.pl
+++ b/nixos/modules/system/boot/loader/grub/install-grub.pl
@@ -34,23 +34,29 @@ sub getList {
 }
 
 sub readFile {
-    my ($fn) = @_; local $/ = undef;
-    open FILE, "<$fn" or return undef; my $s = <FILE>; close FILE;
-    local $/ = "\n"; chomp $s; return $s;
+    my ($fn) = @_;
+    # enable slurp mode: read entire file in one go
+    local $/ = undef;
+    open my $fh, "<$fn" or return undef;
+    my $s = <$fh>;
+    close $fh;
+    # disable slurp mode
+    local $/ = "\n";
+    chomp $s;
+    return $s;
 }
 
 sub writeFile {
     my ($fn, $s) = @_;
-    open FILE, ">$fn" or die "cannot create $fn: $!\n";
-    print FILE $s or die;
-    close FILE or die;
+    open my $fh, ">$fn" or die "cannot create $fn: $!\n";
+    print $fh $s or die "cannot write to $fn: $!\n";
+    close $fh or die "cannot close $fn: $!\n";
 }
 
 sub runCommand {
-    my ($cmd) = @_;
-    open FILE, "$cmd 2>/dev/null |" or die "Failed to execute: $cmd\n";
-    my @ret = <FILE>;
-    close FILE;
+    open(my $fh, "-|", @_) or die "Failed to execute: $@_\n";
+    my @ret = $fh->getlines();
+    close $fh;
     return ($?, @ret);
 }
 
@@ -200,7 +206,7 @@ sub GrubFs {
                 $search = $types{$fsIdentifier} . ' ';
 
                 # Based on the type pull in the identifier from the system
-                my ($status, @devInfo) = runCommand("@utillinux@/bin/blkid -o export @{[$fs->device]}");
+                my ($status, @devInfo) = runCommand("@utillinux@/bin/blkid", "-o", "export", @{[$fs->device]});
                 if ($status != 0) {
                     die "Failed to get blkid info (returned $status) for @{[$fs->mount]} on @{[$fs->device]}";
                 }
@@ -213,7 +219,7 @@ sub GrubFs {
 
             # BTRFS is a special case in that we need to fix the referrenced path based on subvolumes
             if ($fs->type eq 'btrfs') {
-                my ($status, @id_info) = runCommand("@btrfsprogs@/bin/btrfs subvol show @{[$fs->mount]}");
+                my ($status, @id_info) = runCommand("@btrfsprogs@/bin/btrfs", "subvol", "show", @{[$fs->mount]});
                 if ($status != 0) {
                     die "Failed to retrieve subvolume info for @{[$fs->mount]}\n";
                 }
@@ -221,7 +227,7 @@ sub GrubFs {
                 if ($#ids > 0) {
                     die "Btrfs subvol name for @{[$fs->device]} listed multiple times in mount\n"
                 } elsif ($#ids == 0) {
-                    my ($status, @path_info) = runCommand("@btrfsprogs@/bin/btrfs subvol list @{[$fs->mount]}");
+                    my ($status, @path_info) = runCommand("@btrfsprogs@/bin/btrfs", "subvol", "list", @{[$fs->mount]});
                     if ($status != 0) {
                         die "Failed to find @{[$fs->mount]} subvolume id from btrfs\n";
                     }
@@ -442,7 +448,7 @@ sub copyToKernelsDir {
 }
 
 sub addEntry {
-    my ($name, $path, $options) = @_;
+    my ($name, $path, $options, $current) = @_;
     return unless -e "$path/kernel" && -e "$path/initrd";
 
     my $kernel = copyToKernelsDir(Cwd::abs_path("$path/kernel"));
@@ -450,20 +456,28 @@ sub addEntry {
 
     # Include second initrd with secrets
     if (-e -x "$path/append-initrd-secrets") {
-        my $initrdName = basename($initrd);
-        my $initrdSecretsPath = "$bootPath/kernels/$initrdName-secrets";
+        # Name the initrd secrets after the system from which they're derived.
+        my $systemName = basename(Cwd::abs_path("$path"));
+        my $initrdSecretsPath = "$bootPath/kernels/$systemName-secrets";
 
         mkpath(dirname($initrdSecretsPath), 0, 0755);
         my $oldUmask = umask;
         # Make sure initrd is not world readable (won't work if /boot is FAT)
         umask 0137;
         my $initrdSecretsPathTemp = File::Temp::mktemp("$initrdSecretsPath.XXXXXXXX");
-        system("$path/append-initrd-secrets", $initrdSecretsPathTemp) == 0 or die "failed to create initrd secrets: $!\n";
+        if (system("$path/append-initrd-secrets", $initrdSecretsPathTemp) != 0) {
+          if ($current) {
+              die "failed to create initrd secrets $!\n";
+          } else {
+              say STDERR "warning: failed to create initrd secrets for \"$name\", an older generation";
+              say STDERR "note: this is normal after having removed or renamed a file in `boot.initrd.secrets`";
+          }
+        }
         # Check whether any secrets were actually added
         if (-e $initrdSecretsPathTemp && ! -z _) {
             rename $initrdSecretsPathTemp, $initrdSecretsPath or die "failed to move initrd secrets into place: $!\n";
             $copied{$initrdSecretsPath} = 1;
-            $initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$initrdName-secrets";
+            $initrd .= " " . ($grubBoot->path eq "/" ? "" : $grubBoot->path) . "/kernels/$systemName-secrets";
         } else {
             unlink $initrdSecretsPathTemp;
             rmdir dirname($initrdSecretsPathTemp);
@@ -491,7 +505,7 @@ sub addEntry {
         }
         $conf .= "\n";
     } else {
-        $conf .= "menuentry \"$name\" " . ($options||"") . " {\n";
+        $conf .= "menuentry \"$name\" " . $options . " {\n";
         if ($saveDefault) {
             $conf .= "  savedefault\n";
         }
@@ -511,7 +525,7 @@ sub addEntry {
 # Add default entries.
 $conf .= "$extraEntries\n" if $extraEntriesBeforeNixOS;
 
-addEntry("NixOS - Default", $defaultConfig, $entryOptions);
+addEntry("@distroName@ - Default", $defaultConfig, $entryOptions, 1);
 
 $conf .= "$extraEntries\n" unless $extraEntriesBeforeNixOS;
 
@@ -536,7 +550,7 @@ foreach my $link (@links) {
         my $linkname = basename($link);
         $entryName = "($linkname - $date - $version)";
     }
-    addEntry("NixOS - $entryName", $link);
+    addEntry("@distroName@ - $entryName", $link, "", 1);
 }
 
 my $grubBootPath = $grubBoot->path;
@@ -568,19 +582,19 @@ 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, $subEntryOptions);
+        addEntry("@distroName@ - Configuration " . nrFromGen($link) . " ($date - $version)", $link, $subEntryOptions, 0);
     }
 
     $conf .= "}\n" if $grubVersion == 2;
 }
 
-addProfile "/nix/var/nix/profiles/system", "NixOS - All configurations";
+addProfile "/nix/var/nix/profiles/system", "@distroName@ - All configurations";
 
 if ($grubVersion == 2) {
     for my $profile (glob "/nix/var/nix/profiles/system-profiles/*") {
         my $name = basename($profile);
         next unless $name =~ /^\w+$/;
-        addProfile $profile, "NixOS - Profile '$name'";
+        addProfile $profile, "@distroName@ - Profile '$name'";
     }
 }
 
diff --git a/nixos/modules/system/boot/loader/init-script/init-script-builder.sh b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh
index bd3fc64999d..755ea259c42 100644
--- a/nixos/modules/system/boot/loader/init-script/init-script-builder.sh
+++ b/nixos/modules/system/boot/loader/init-script/init-script-builder.sh
@@ -64,13 +64,13 @@ addEntry() {
 
 mkdir -p /boot /sbin
 
-addEntry "NixOS - Default" $defaultConfig ""
+addEntry "@distroName@ - Default" $defaultConfig ""
 
 # Add all generations of the system profile to the menu, in reverse
 # (most recent to least recent) order.
 for link in $((ls -d $defaultConfig/specialisation/* ) | sort -n); do
     date=$(stat --printf="%y\n" $link | sed 's/\..*//')
-    addEntry "NixOS - variation" $link ""
+    addEntry "@distroName@ - variation" $link ""
 done
 
 for generation in $(
@@ -85,7 +85,7 @@ for generation in $(
     else
       suffix="($date)"
     fi
-    addEntry "NixOS - Configuration $generation $suffix" $link "$generation ($date)"
+    addEntry "@distroName@ - Configuration $generation $suffix" $link "$generation ($date)"
 done
 
 mv $tmpOther $targetOther
diff --git a/nixos/modules/system/boot/loader/init-script/init-script.nix b/nixos/modules/system/boot/loader/init-script/init-script.nix
index 8287131d321..4d33ed6b665 100644
--- a/nixos/modules/system/boot/loader/init-script/init-script.nix
+++ b/nixos/modules/system/boot/loader/init-script/init-script.nix
@@ -8,6 +8,7 @@ let
     src = ./init-script-builder.sh;
     isExecutable = true;
     inherit (pkgs) bash;
+    inherit (config.system.nixos) distroName;
     path = [pkgs.coreutils pkgs.gnused pkgs.gnugrep];
   };
 
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 68da2061591..a040518a5a5 100644..100755
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
@@ -16,6 +16,7 @@ import datetime
 import glob
 import os.path
 from typing import NamedTuple, List, Optional
+from packaging import version
 
 class SystemIdentifier(NamedTuple):
     profile: Optional[str]
@@ -41,7 +42,7 @@ def system_dir(profile: Optional[str], generation: int, specialisation: Optional
     else:
         return d
 
-BOOT_ENTRY = """title NixOS{profile}{specialisation}
+BOOT_ENTRY = """title {title}
 version Generation {generation} {description}
 linux {kernel}
 initrd {initrd}
@@ -84,51 +85,64 @@ def copy_from_profile(profile: Optional[str], generation: int, specialisation: O
     return efi_file_path
 
 
-def describe_generation(generation_dir: str) -> str:
+def describe_generation(profile: Optional[str], generation: int, specialisation: Optional[str]) -> str:
     try:
-        with open("%s/nixos-version" % generation_dir) as f:
+        with open(profile_path(profile, generation, specialisation, "nixos-version")) as f:
             nixos_version = f.read()
     except IOError:
         nixos_version = "Unknown"
 
-    kernel_dir = os.path.dirname(os.path.realpath("%s/kernel" % generation_dir))
+    kernel_dir = os.path.dirname(profile_path(profile, generation, specialisation, "kernel"))
     module_dir = glob.glob("%s/lib/modules/*" % kernel_dir)[0]
     kernel_version = os.path.basename(module_dir)
 
-    build_time = int(os.path.getctime(generation_dir))
+    build_time = int(os.path.getctime(system_dir(profile, generation, specialisation)))
     build_date = datetime.datetime.fromtimestamp(build_time).strftime('%F')
 
-    description = "NixOS {}, Linux Kernel {}, Built on {}".format(
+    description = "@distroName@ {}, Linux Kernel {}, Built on {}".format(
         nixos_version, kernel_version, build_date
     )
 
     return description
 
 
-def write_entry(profile: Optional[str], generation: int, specialisation: Optional[str], machine_id: str) -> None:
+def write_entry(profile: Optional[str], generation: int, specialisation: Optional[str],
+                machine_id: str, current: bool) -> None:
     kernel = copy_from_profile(profile, generation, specialisation, "kernel")
     initrd = copy_from_profile(profile, generation, specialisation, "initrd")
+
+    title = "@distroName@{profile}{specialisation}".format(
+        profile=" [" + profile + "]" if profile else "",
+        specialisation=" (%s)" % specialisation if specialisation else "")
+
     try:
         append_initrd_secrets = profile_path(profile, generation, specialisation, "append-initrd-secrets")
         subprocess.check_call([append_initrd_secrets, "@efiSysMountPoint@%s" % (initrd)])
     except FileNotFoundError:
         pass
+    except subprocess.CalledProcessError:
+        if current:
+            print("failed to create initrd secrets!", file=sys.stderr)
+            sys.exit(1)
+        else:
+            print("warning: failed to create initrd secrets "
+                  f'for "{title} - Configuration {generation}", an older generation', file=sys.stderr)
+            print("note: this is normal after having removed "
+                  "or renamed a file in `boot.initrd.secrets`", file=sys.stderr)
     entry_file = "@efiSysMountPoint@/loader/entries/%s" % (
         generation_conf_filename(profile, generation, specialisation))
-    generation_dir = os.readlink(system_dir(profile, generation, specialisation))
     tmp_path = "%s.tmp" % (entry_file)
-    kernel_params = "init=%s/init " % generation_dir
+    kernel_params = "init=%s " % profile_path(profile, generation, specialisation, "init")
 
-    with open("%s/kernel-params" % (generation_dir)) as params_file:
+    with open(profile_path(profile, generation, specialisation, "kernel-params")) as params_file:
         kernel_params = kernel_params + params_file.read()
     with open(tmp_path, 'w') as f:
-        f.write(BOOT_ENTRY.format(profile=" [" + profile + "]" if profile else "",
-                    specialisation=" (%s)" % specialisation if specialisation else "",
+        f.write(BOOT_ENTRY.format(title=title,
                     generation=generation,
                     kernel=kernel,
                     initrd=initrd,
                     kernel_params=kernel_params,
-                    description=describe_generation(generation_dir)))
+                    description=describe_generation(profile, generation, specialisation)))
         if machine_id is not None:
             f.write("machine-id %s\n" % machine_id)
     os.rename(tmp_path, entry_file)
@@ -205,8 +219,8 @@ def get_profiles() -> List[str]:
         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')
+    parser = argparse.ArgumentParser(description='Update @distroName@-related systemd-boot files')
+    parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default @distroName@ config to boot')
     args = parser.parse_args()
 
     try:
@@ -227,20 +241,21 @@ def main() -> None:
         warnings.warn("NIXOS_INSTALL_GRUB env var deprecated, use NIXOS_INSTALL_BOOTLOADER", DeprecationWarning)
         os.environ["NIXOS_INSTALL_BOOTLOADER"] = "1"
 
+    # flags to pass to bootctl install/update
+    bootctl_flags = []
+
+    if "@canTouchEfiVariables@" != "1":
+        bootctl_flags.append("--no-variables")
+
+    if "@graceful@" == "1":
+        bootctl_flags.append("--graceful")
+
     if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1":
         # bootctl uses fopen() with modes "wxe" and fails if the file exists.
         if os.path.exists("@efiSysMountPoint@/loader/loader.conf"):
             os.unlink("@efiSysMountPoint@/loader/loader.conf")
 
-        flags = []
-
-        if "@canTouchEfiVariables@" != "1":
-            flags.append("--no-variables")
-
-        if "@graceful@" == "1":
-            flags.append("--graceful")
-
-        subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + flags + ["install"])
+        subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + bootctl_flags + ["install"])
     else:
         # Update bootloader to latest if needed
         available_out = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[2]
@@ -258,12 +273,18 @@ def main() -> None:
         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)
+        installed_version = version.parse(installed_match.group(1))
+        available_version = version.parse(available_match.group(1))
 
+        # systemd 252 has a regression that leaves some machines unbootable, so we skip that update.
+        # The fix is in 252.2
+        # See https://github.com/systemd/systemd/issues/25363 and https://github.com/NixOS/nixpkgs/pull/201558#issuecomment-1348603263
         if installed_version < available_version:
-            print("updating systemd-boot from %s to %s" % (installed_version, available_version))
-            subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@", "update"])
+            if version.parse('252') <= available_version < version.parse('252.2'):
+                print("skipping systemd-boot update to %s because of known regression" % available_version)
+            else:
+                print("updating systemd-boot from %s to %s" % (installed_version, available_version))
+                subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + bootctl_flags + ["update"])
 
     mkdir_p("@efiSysMountPoint@/efi/nixos")
     mkdir_p("@efiSysMountPoint@/loader/entries")
@@ -274,14 +295,19 @@ def main() -> None:
     remove_old_entries(gens)
     for gen in gens:
         try:
-            write_entry(*gen, machine_id)
+            is_default = os.path.dirname(profile_path(*gen, "init")) == args.default_config
+            write_entry(*gen, machine_id, current=is_default)
             for specialisation in get_specialisations(*gen):
-                write_entry(*specialisation, machine_id)
-            if os.readlink(system_dir(*gen)) == args.default_config:
+                write_entry(*specialisation, machine_id, current=is_default)
+            if is_default:
                 write_loader_conf(*gen)
         except OSError as e:
-            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)
+            # See https://github.com/NixOS/nixpkgs/issues/114552
+            if e.errno == errno.EINVAL:
+                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)
+            else:
+                raise e
 
     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 8cb7c7b8e47..8a3e89e5888 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
@@ -7,18 +7,20 @@ let
 
   efi = config.boot.loader.efi;
 
+  python3 = pkgs.python3.withPackages (ps: [ ps.packaging ]);
+
   systemdBootBuilder = pkgs.substituteAll {
     src = ./systemd-boot-builder.py;
 
     isExecutable = true;
 
-    inherit (pkgs) python3;
+    inherit python3;
 
     systemd = config.systemd.package;
 
     nix = config.nix.package.out;
 
-    timeout = if config.boot.loader.timeout != null then config.boot.loader.timeout else "";
+    timeout = optionalString (config.boot.loader.timeout != null) config.boot.loader.timeout;
 
     editor = if cfg.editor then "True" else "False";
 
@@ -28,9 +30,11 @@ let
 
     inherit (efi) efiSysMountPoint canTouchEfiVariables;
 
-    memtest86 = if cfg.memtest86.enable then pkgs.memtest86-efi else "";
+    inherit (config.system.nixos) distroName;
+
+    memtest86 = optionalString cfg.memtest86.enable pkgs.memtest86-efi;
 
-    netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else "";
+    netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi;
 
     copyExtraFiles = pkgs.writeShellScript "copy-extra-files" ''
       empty_file=$(${pkgs.coreutils}/bin/mktemp)
@@ -48,7 +52,7 @@ let
   };
 
   checkedSystemdBootBuilder = pkgs.runCommand "systemd-boot" {
-    nativeBuildInputs = [ pkgs.mypy ];
+    nativeBuildInputs = [ pkgs.mypy python3 ];
   } ''
     install -m755 ${systemdBootBuilder} $out
     mypy \
diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix
index 03d03cb348e..b8f36538e70 100644
--- a/nixos/modules/system/boot/luksroot.nix
+++ b/nixos/modules/system/boot/luksroot.nix
@@ -158,6 +158,20 @@ let
       wait_target "header" ${dev.header} || die "${dev.header} is unavailable"
     ''}
 
+    try_empty_passphrase() {
+        ${if dev.tryEmptyPassphrase then ''
+             echo "Trying empty passphrase!"
+             echo "" | ${csopen}
+             cs_status=$?
+             if [ $cs_status -eq 0 ]; then
+                 return 0
+             else
+                 return 1
+             fi
+        '' else "return 1"}
+    }
+
+
     do_open_passphrase() {
         local passphrase
 
@@ -212,13 +226,27 @@ let
             ${csopen} --key-file=${dev.keyFile} \
               ${optionalString (dev.keyFileSize != null) "--keyfile-size=${toString dev.keyFileSize}"} \
               ${optionalString (dev.keyFileOffset != null) "--keyfile-offset=${toString dev.keyFileOffset}"}
+            cs_status=$?
+            if [ $cs_status -ne 0 ]; then
+              echo "Key File ${dev.keyFile} failed!"
+              if ! try_empty_passphrase; then
+                ${if dev.fallbackToPassword then "echo" else "die"} "${dev.keyFile} is unavailable"
+                echo " - failing back to interactive password prompt"
+                do_open_passphrase
+              fi
+            fi
         else
-            ${if dev.fallbackToPassword then "echo" else "die"} "${dev.keyFile} is unavailable"
-            echo " - failing back to interactive password prompt"
-            do_open_passphrase
+            # If the key file never shows up we should also try the empty passphrase
+            if ! try_empty_passphrase; then
+               ${if dev.fallbackToPassword then "echo" else "die"} "${dev.keyFile} is unavailable"
+               echo " - failing back to interactive password prompt"
+               do_open_passphrase
+            fi
         fi
         '' else ''
-        do_open_passphrase
+           if ! try_empty_passphrase; then
+              do_open_passphrase
+           fi
         ''}
     }
 
@@ -476,6 +504,7 @@ 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"
@@ -483,6 +512,8 @@ let
       ++ optional (v.header != null) "header=${v.header}"
       ++ optional (v.keyFileOffset != null) "keyfile-offset=${toString v.keyFileOffset}"
       ++ optional (v.keyFileSize != null) "keyfile-size=${toString v.keyFileSize}"
+      ++ optional (v.keyFileTimeout != null) "keyfile-timeout=${builtins.toString v.keyFileTimeout}s"
+      ++ optional (v.tryEmptyPassphrase) "try-empty-password=true"
     ;
   in "${n} ${v.device} ${if v.keyFile == null then "-" else v.keyFile} ${lib.concatStringsSep "," opts}") luks.devices));
 
@@ -594,6 +625,25 @@ in
             '';
           };
 
+          tryEmptyPassphrase = mkOption {
+            default = false;
+            type = types.bool;
+            description = lib.mdDoc ''
+              If keyFile fails then try an empty passphrase first before
+              prompting for password.
+            '';
+          };
+
+          keyFileTimeout = mkOption {
+            default = null;
+            example = 5;
+            type = types.nullOr types.int;
+            description = lib.mdDoc ''
+              The amount of time in seconds for a keyFile to appear before
+              timing out and trying passwords.
+            '';
+          };
+
           keyFileSize = mkOption {
             default = null;
             example = 4096;
@@ -889,6 +939,10 @@ in
           message = "boot.initrd.luks.devices.<name>.bypassWorkqueues is not supported for kernels older than 5.9";
         }
 
+        { assertion = !config.boot.initrd.systemd.enable -> all (x: x.keyFileTimeout == null) (attrValues luks.devices);
+          message = "boot.initrd.luks.devices.<name>.keyFileTimeout is only supported for systemd initrd";
+        }
+
         { 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.";
         }
@@ -929,7 +983,14 @@ in
       ++ (if builtins.elem "xts" luks.cryptoModules then ["ecb"] else []);
 
     # copy the cryptsetup binary and it's dependencies
-    boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
+    boot.initrd.extraUtilsCommands = let
+      pbkdf2-sha512 = pkgs.runCommandCC "pbkdf2-sha512" { buildInputs = [ pkgs.openssl ]; } ''
+        mkdir -p "$out/bin"
+        cc -O3 -lcrypto ${./pbkdf2-sha512.c} -o "$out/bin/pbkdf2-sha512"
+        strip -s "$out/bin/pbkdf2-sha512"
+      '';
+    in
+    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
@@ -939,9 +1000,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${lib.getLib pkgs.openssl}/lib ${./pbkdf2-sha512.c} -o pbkdf2-sha512 -lcrypto
-        strip -s pbkdf2-sha512
-        copy_bin_and_libs pbkdf2-sha512
+        copy_bin_and_libs ${pbkdf2-sha512}/bin/pbkdf2-sha512
 
         mkdir -p $out/etc/ssl
         cp -pdv ${pkgs.openssl.out}/etc/ssl/openssl.cnf $out/etc/ssl
@@ -965,13 +1024,12 @@ in
         copy_bin_and_libs ${pkgs.gnupg}/libexec/scdaemon
 
         ${concatMapStringsSep "\n" (x:
-          if x.gpgCard != null then
+          optionalString (x.gpgCard != null)
             ''
               mkdir -p $out/secrets/gpg-keys/${x.device}
               cp -a ${x.gpgCard.encryptedPass} $out/secrets/gpg-keys/${x.device}/cryptkey.gpg
               cp -a ${x.gpgCard.publicKey} $out/secrets/gpg-keys/${x.device}/pubkey.asc
             ''
-          else ""
           ) (attrValues luks.devices)
         }
       ''}
diff --git a/nixos/modules/system/boot/modprobe.nix b/nixos/modules/system/boot/modprobe.nix
index 54bb7ea9ddd..d751c4462d3 100644
--- a/nixos/modules/system/boot/modprobe.nix
+++ b/nixos/modules/system/boot/modprobe.nix
@@ -7,7 +7,7 @@ with lib;
   ###### interface
 
   options = {
-    boot.modprobeConfig.enable = mkEnableOption (lib.mdDoc "modprobe config. This is useful for systemds like containers which do not require a kernel.") // {
+    boot.modprobeConfig.enable = mkEnableOption (lib.mdDoc "modprobe config. This is useful for systems like containers which do not require a kernel") // {
       default = true;
     };
 
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index 188f2f64dc8..52413a13f07 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -6,8 +6,6 @@ with lib;
 
 let
 
-  cfg = config.systemd.network;
-
   check = {
 
     global = {
@@ -72,6 +70,9 @@ let
           "CombinedChannels"
           "RxBufferSize"
           "TxBufferSize"
+          "ReceiveQueues"
+          "TransmitQueues"
+          "TransmitQueueLength"
         ])
         (assertValueOneOf "MACAddressPolicy" ["persistent" "random" "none"])
         (assertMacAddress "MACAddress")
@@ -98,6 +99,9 @@ let
         (assertRange "CombinedChannels" 1 4294967295)
         (assertInt "RxBufferSize")
         (assertInt "TxBufferSize")
+        (assertRange "ReceiveQueues" 1 4096)
+        (assertRange "TransmitQueues" 1 4096)
+        (assertRange "TransmitQueueLength" 1 4294967294)
       ];
     };
 
@@ -303,6 +307,48 @@ let
 
       sectionTap = checkUnitConfig "Tap" tunChecks;
 
+      sectionL2TP = checkUnitConfig "L2TP" [
+        (assertOnlyFields [
+          "TunnelId"
+          "PeerTunnelId"
+          "Remote"
+          "Local"
+          "EncapsulationType"
+          "UDPSourcePort"
+          "UDPDestinationPort"
+          "UDPChecksum"
+          "UDP6ZeroChecksumTx"
+          "UDP6ZeroChecksumRx"
+        ])
+        (assertInt "TunnelId")
+        (assertRange "TunnelId" 1 4294967295)
+        (assertInt "PeerTunnelId")
+        (assertRange "PeerTunnelId" 1 4294967295)
+        (assertValueOneOf "EncapsulationType" [ "ip" "udp" ])
+        (assertPort "UDPSourcePort")
+        (assertPort "UDPDestinationPort")
+        (assertValueOneOf "UDPChecksum" boolValues)
+        (assertValueOneOf "UDP6ZeroChecksumTx" boolValues)
+        (assertValueOneOf "UDP6ZeroChecksumRx" boolValues)
+      ];
+
+      sectionL2TPSession = checkUnitConfig "L2TPSession" [
+        (assertOnlyFields [
+          "Name"
+          "SessionId"
+          "PeerSessionId"
+          "Layer2SpecificHeader"
+        ])
+        (assertHasField "Name")
+        (assertHasField "SessionId")
+        (assertInt "SessionId")
+        (assertRange "SessionId" 1 4294967295)
+        (assertHasField "PeerSessionId")
+        (assertInt "PeerSessionId")
+        (assertRange "PeerSessionId" 1 4294967295)
+        (assertValueOneOf "Layer2SpecificHeader" [ "none" "default" ])
+      ];
+
       # NOTE The PrivateKey directive is missing on purpose here, please
       # do not add it to this list. The nix store is world-readable let's
       # refrain ourselves from providing a footgun.
@@ -918,6 +964,470 @@ let
         (assertMacAddress "MACAddress")
       ];
 
+      sectionBridge = checkUnitConfig "Bridge" [
+        (assertOnlyFields [
+          "UnicastFlood"
+          "MulticastFlood"
+          "MulticastToUnicast"
+          "NeighborSuppression"
+          "Learning"
+          "Hairpin"
+          "Isolated"
+          "UseBPDU"
+          "FastLeave"
+          "AllowPortToBeRoot"
+          "ProxyARP"
+          "ProxyARPWiFi"
+          "MulticastRouter"
+          "Cost"
+          "Priority"
+        ])
+        (assertValueOneOf "UnicastFlood" boolValues)
+        (assertValueOneOf "MulticastFlood" boolValues)
+        (assertValueOneOf "MulticastToUnicast" boolValues)
+        (assertValueOneOf "NeighborSuppression" boolValues)
+        (assertValueOneOf "Learning" boolValues)
+        (assertValueOneOf "Hairpin" boolValues)
+        (assertValueOneOf "Isolated" boolValues)
+        (assertValueOneOf "UseBPDU" boolValues)
+        (assertValueOneOf "FastLeave" boolValues)
+        (assertValueOneOf "AllowPortToBeRoot" boolValues)
+        (assertValueOneOf "ProxyARP" boolValues)
+        (assertValueOneOf "ProxyARPWiFi" boolValues)
+        (assertValueOneOf "MulticastRouter" [ "no" "query" "permanent" "temporary" ])
+        (assertInt "Cost")
+        (assertRange "Cost" 1 65535)
+        (assertInt "Priority")
+        (assertRange "Priority" 0 63)
+      ];
+
+      sectionBridgeFDB = checkUnitConfig "BridgeFDB" [
+        (assertOnlyFields [
+          "MACAddress"
+          "Destination"
+          "VLANId"
+          "VNI"
+          "AssociatedWith"
+          "OutgoingInterface"
+        ])
+        (assertHasField "MACAddress")
+        (assertInt "VLANId")
+        (assertRange "VLANId" 0 4094)
+        (assertInt "VNI")
+        (assertRange "VNI" 1 16777215)
+        (assertValueOneOf "AssociatedWith" [ "use" "self" "master" "router" ])
+      ];
+
+      sectionBridgeMDB = checkUnitConfig "BridgeMDB" [
+        (assertOnlyFields [
+          "MulticastGroupAddress"
+          "VLANId"
+        ])
+        (assertHasField "MulticastGroupAddress")
+        (assertInt "VLANId")
+        (assertRange "VLANId" 0 4094)
+      ];
+
+      sectionLLDP = checkUnitConfig "LLDP" [
+        (assertOnlyFields [
+          "MUDURL"
+        ])
+      ];
+
+      sectionCAN = checkUnitConfig "CAN" [
+        (assertOnlyFields [
+          "BitRate"
+          "SamplePoint"
+          "TimeQuantaNSec"
+          "PropagationSegment"
+          "PhaseBufferSegment1"
+          "PhaseBufferSegment2"
+          "SyncJumpWidth"
+          "DataBitRate"
+          "DataSamplePoint"
+          "DataTimeQuantaNSec"
+          "DataPropagationSegment"
+          "DataPhaseBufferSegment1"
+          "DataPhaseBufferSegment2"
+          "DataSyncJumpWidth"
+          "FDMode"
+          "FDNonISO"
+          "RestartSec"
+          "Termination"
+          "TripleSampling"
+          "BusErrorReporting"
+          "ListenOnly"
+          "Loopback"
+          "OneShot"
+          "PresumeAck"
+          "ClassicDataLengthCode"
+        ])
+        (assertInt "TimeQuantaNSec" )
+        (assertRange "TimeQuantaNSec" 0 4294967295 )
+        (assertInt "PropagationSegment" )
+        (assertRange "PropagationSegment" 0 4294967295 )
+        (assertInt "PhaseBufferSegment1" )
+        (assertRange "PhaseBufferSegment1" 0 4294967295 )
+        (assertInt "PhaseBufferSegment2" )
+        (assertRange "PhaseBufferSegment2" 0 4294967295 )
+        (assertInt "SyncJumpWidth" )
+        (assertRange "SyncJumpWidth" 0 4294967295 )
+        (assertInt "DataTimeQuantaNSec" )
+        (assertRange "DataTimeQuantaNSec" 0 4294967295 )
+        (assertInt "DataPropagationSegment" )
+        (assertRange "DataPropagationSegment" 0 4294967295 )
+        (assertInt "DataPhaseBufferSegment1" )
+        (assertRange "DataPhaseBufferSegment1" 0 4294967295 )
+        (assertInt "DataPhaseBufferSegment2" )
+        (assertRange "DataPhaseBufferSegment2" 0 4294967295 )
+        (assertInt "DataSyncJumpWidth" )
+        (assertRange "DataSyncJumpWidth" 0 4294967295 )
+        (assertValueOneOf "FDMode" boolValues)
+        (assertValueOneOf "FDNonISO" boolValues)
+        (assertValueOneOf "TripleSampling" boolValues)
+        (assertValueOneOf "BusErrorReporting" boolValues)
+        (assertValueOneOf "ListenOnly" boolValues)
+        (assertValueOneOf "Loopback" boolValues)
+        (assertValueOneOf "OneShot" boolValues)
+        (assertValueOneOf "PresumeAck" boolValues)
+        (assertValueOneOf "ClassicDataLengthCode" boolValues)
+      ];
+
+      sectionIPoIB = checkUnitConfig "IPoIB" [
+        (assertOnlyFields [
+          "Mode"
+          "IgnoreUserspaceMulticastGroup"
+        ])
+        (assertValueOneOf "Mode" [ "datagram" "connected" ])
+        (assertValueOneOf "IgnoreUserspaceMulticastGroup" boolValues)
+      ];
+
+      sectionQDisc = checkUnitConfig "QDisc" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+        ])
+        (assertValueOneOf "Parent" [ "clsact" "ingress" ])
+      ];
+
+      sectionNetworkEmulator = checkUnitConfig "NetworkEmulator" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "DelaySec"
+          "DelayJitterSec"
+          "PacketLimit"
+          "LossRate"
+          "DuplicateRate"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 0 4294967294)
+      ];
+
+      sectionTokenBucketFilter = checkUnitConfig "TokenBucketFilter" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "LatencySec"
+          "LimitBytes"
+          "BurstBytes"
+          "Rate"
+          "MPUBytes"
+          "PeakRate"
+          "MTUBytes"
+        ])
+      ];
+
+      sectionPIE = checkUnitConfig "PIE" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 1 4294967294)
+      ];
+
+      sectionFlowQueuePIE = checkUnitConfig "FlowQueuePIE" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 1 4294967294)
+      ];
+
+      sectionStochasticFairBlue = checkUnitConfig "StochasticFairBlue" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 1 4294967294)
+      ];
+
+      sectionStochasticFairnessQueueing = checkUnitConfig "StochasticFairnessQueueing" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PerturbPeriodSec"
+        ])
+        (assertInt "PerturbPeriodSec")
+      ];
+
+      sectionBFIFO = checkUnitConfig "BFIFO" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "LimitBytes"
+        ])
+      ];
+
+      sectionPFIFO = checkUnitConfig "PFIFO" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 0 4294967294)
+      ];
+
+      sectionPFIFOHeadDrop = checkUnitConfig "PFIFOHeadDrop" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 0 4294967294)
+      ];
+
+      sectionPFIFOFast = checkUnitConfig "PFIFOFast" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+        ])
+      ];
+
+      sectionCAKE = checkUnitConfig "CAKE" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "Bandwidth"
+          "AutoRateIngress"
+          "OverheadBytes"
+          "MPUBytes"
+          "CompensationMode"
+          "UseRawPacketSize"
+          "FlowIsolationMode"
+          "NAT"
+          "PriorityQueueingPreset"
+          "FirewallMark"
+          "Wash"
+          "SplitGSO"
+        ])
+        (assertValueOneOf "AutoRateIngress" boolValues)
+        (assertInt "OverheadBytes")
+        (assertRange "OverheadBytes" (-64) 256)
+        (assertInt "MPUBytes")
+        (assertRange "MPUBytes" 1 256)
+        (assertValueOneOf "CompensationMode" [ "none" "atm" "ptm" ])
+        (assertValueOneOf "UseRawPacketSize" boolValues)
+        (assertValueOneOf "FlowIsolationMode"
+          [
+            "none"
+            "src-host"
+            "dst-host"
+            "hosts"
+            "flows"
+            "dual-src-host"
+            "dual-dst-host"
+            "triple"
+          ])
+        (assertValueOneOf "NAT" boolValues)
+        (assertValueOneOf "PriorityQueueingPreset"
+          [
+            "besteffort"
+            "precedence"
+            "diffserv8"
+            "diffserv4"
+            "diffserv3"
+          ])
+        (assertInt "FirewallMark")
+        (assertRange "FirewallMark" 1 4294967295)
+        (assertValueOneOf "Wash" boolValues)
+        (assertValueOneOf "SplitGSO" boolValues)
+      ];
+
+      sectionControlledDelay = checkUnitConfig "ControlledDelay" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+          "TargetSec"
+          "IntervalSec"
+          "ECN"
+          "CEThresholdSec"
+        ])
+        (assertValueOneOf "ECN" boolValues)
+      ];
+
+      sectionDeficitRoundRobinScheduler = checkUnitConfig "DeficitRoundRobinScheduler" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+        ])
+      ];
+
+      sectionDeficitRoundRobinSchedulerClass = checkUnitConfig "DeficitRoundRobinSchedulerClass" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "QuantumBytes"
+        ])
+      ];
+
+      sectionEnhancedTransmissionSelection = checkUnitConfig "EnhancedTransmissionSelection" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "Bands"
+          "StrictBands"
+          "QuantumBytes"
+          "PriorityMap"
+        ])
+        (assertInt "Bands")
+        (assertRange "Bands" 1 16)
+        (assertInt "StrictBands")
+        (assertRange "StrictBands" 1 16)
+      ];
+
+      sectionGenericRandomEarlyDetection = checkUnitConfig "GenericRandomEarlyDetection" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "VirtualQueues"
+          "DefaultVirtualQueue"
+          "GenericRIO"
+        ])
+        (assertInt "VirtualQueues")
+        (assertRange "VirtualQueues" 1 16)
+        (assertInt "DefaultVirtualQueue")
+        (assertRange "DefaultVirtualQueue" 1 16)
+        (assertValueOneOf "GenericRIO" boolValues)
+      ];
+
+      sectionFairQueueingControlledDelay = checkUnitConfig "FairQueueingControlledDelay" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+          "MemoryLimitBytes"
+          "Flows"
+          "TargetSec"
+          "IntervalSec"
+          "QuantumBytes"
+          "ECN"
+          "CEThresholdSec"
+        ])
+        (assertInt "PacketLimit")
+        (assertInt "Flows")
+        (assertValueOneOf "ECN" boolValues)
+      ];
+
+      sectionFairQueueing = checkUnitConfig "FairQueueing" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+          "FlowLimit"
+          "QuantumBytes"
+          "InitualQuantumBytes"
+          "MaximumRate"
+          "Buckets"
+          "OrphanMask"
+          "Pacing"
+          "CEThresholdSec"
+        ])
+        (assertInt "PacketLimit")
+        (assertInt "FlowLimit")
+        (assertInt "OrphanMask")
+        (assertValueOneOf "Pacing" boolValues)
+      ];
+
+      sectionTrivialLinkEqualizer = checkUnitConfig "TrivialLinkEqualizer" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "Id"
+        ])
+      ];
+
+      sectionHierarchyTokenBucket = checkUnitConfig "HierarchyTokenBucket" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "DefaultClass"
+          "RateToQuantum"
+        ])
+        (assertInt "RateToQuantum")
+      ];
+
+      sectionHierarchyTokenBucketClass = checkUnitConfig "HierarchyTokenBucketClass" [
+        (assertOnlyFields [
+          "Parent"
+          "ClassId"
+          "Priority"
+          "QuantumBytes"
+          "MTUBytes"
+          "OverheadBytes"
+          "Rate"
+          "CeilRate"
+          "BufferBytes"
+          "CeilBufferBytes"
+        ])
+      ];
+
+      sectionHeavyHitterFilter = checkUnitConfig "HeavyHitterFilter" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+          "PacketLimit"
+        ])
+        (assertInt "PacketLimit")
+        (assertRange "PacketLimit" 0 4294967294)
+      ];
+
+      sectionQuickFairQueueing = checkUnitConfig "QuickFairQueueing" [
+        (assertOnlyFields [
+          "Parent"
+          "Handle"
+        ])
+      ];
+
+      sectionQuickFairQueueingClass = checkUnitConfig "QuickFairQueueingClass" [
+        (assertOnlyFields [
+          "Parent"
+          "ClassId"
+          "Weight"
+          "MaxPacketBytes"
+        ])
+        (assertInt "Weight")
+        (assertRange "Weight" 1 1023)
+      ];
+
+      sectionBridgeVLAN = checkUnitConfig "BridgeVLAN" [
+        (assertOnlyFields [
+          "VLAN"
+          "EgressUntagged"
+          "PVID"
+        ])
+        (assertInt "PVID")
+        (assertRange "PVID" 0 4094)
+      ];
     };
   };
 
@@ -1012,6 +1522,21 @@ let
 
   };
 
+
+  l2tpSessionOptions = {
+    options = {
+      l2tpSessionConfig = mkOption {
+        default = {};
+        type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionL2TPSession;
+        description = lib.mdDoc ''
+          Each attribute in this set specifies an option in the
+          `[L2TPSession]` section of the unit.  See
+          {manpage}`systemd.netdev(5)` for details.
+        '';
+      };
+    };
+  };
+
   wireguardPeerOptions = {
     options = {
       wireguardPeerConfig = mkOption {
@@ -1125,6 +1650,38 @@ let
       '';
     };
 
+    l2tpConfig = mkOption {
+      default = {};
+      example = {
+        TunnelId = 10;
+        PeerTunnelId = 12;
+        Local = "static";
+        Remote = "192.168.30.101";
+        EncapsulationType = "ip";
+      };
+      type = types.addCheck (types.attrsOf unitOption) check.netdev.sectionL2TP;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[L2TP]` section of the unit. See
+        {manpage}`systemd.netdev(5)` for details.
+      '';
+    };
+
+    l2tpSessions = mkOption {
+      default = [];
+      example = [ { l2tpSessionConfig={
+        SessionId = 25;
+        PeerSessionId = 26;
+        Name = "l2tp-sess";
+      };}];
+      type = with types; listOf (submodule l2tpSessionOptions);
+      description = lib.mdDoc ''
+        Each item in this array specifies an option in the
+        `[L2TPSession]` section of the unit. See
+        {manpage}`systemd.netdev(5)` for details.
+      '';
+    };
+
     wireguardConfig = mkOption {
       default = {};
       example = {
@@ -1306,6 +1863,51 @@ let
     };
   };
 
+  bridgeFDBOptions = {
+    options = {
+      bridgeFDBConfig = mkOption {
+        default = {};
+        example = { MACAddress = "65:43:4a:5b:d8:5f"; Destination = "192.168.1.42"; VNI = 20; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionBridgeFDB;
+        description = lib.mdDoc ''
+          Each attribute in this set specifies an option in the
+          `[BridgeFDB]` section of the unit.  See
+          {manpage}`systemd.network(5)` for details.
+        '';
+      };
+    };
+  };
+
+  bridgeMDBOptions = {
+    options = {
+      bridgeMDBConfig = mkOption {
+        default = {};
+        example = { MulticastGroupAddress = "ff02::1:2:3:4"; VLANId = 10; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionBridgeMDB;
+        description = lib.mdDoc ''
+          Each attribute in this set specifies an option in the
+          `[BridgeMDB]` section of the unit.  See
+          {manpage}`systemd.network(5)` for details.
+        '';
+      };
+    };
+  };
+
+  bridgeVLANOptions = {
+    options = {
+      bridgeMDBConfig = mkOption {
+        default = {};
+        example = { VLAN = 20; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionBridgeVLAN;
+        description = lib.mdDoc ''
+          Each attribute in this set specifies an option in the
+          `[BridgeVLAN]` section of the unit.  See
+          {manpage}`systemd.network(5)` for details.
+        '';
+      };
+    };
+  };
+
   networkOptions = commonNetworkOptions // {
 
     linkConfig = mkOption {
@@ -1445,6 +2047,366 @@ let
       '';
     };
 
+    bridgeConfig = mkOption {
+      default = {};
+      example = { MulticastFlood = false; Cost = 20; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionBridge;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[Bridge]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    bridgeFDBs = mkOption {
+      default = [];
+      example = [ { bridgeFDBConfig = { MACAddress = "90:e2:ba:43:fc:71"; Destination = "192.168.100.4"; VNI = 3600; }; } ];
+      type = with types; listOf (submodule bridgeFDBOptions);
+      description = lib.mdDoc ''
+        A list of BridgeFDB sections to be added to the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    bridgeMDBs = mkOption {
+      default = [];
+      example = [ { bridgeMDBConfig = { MulticastGroupAddress = "ff02::1:2:3:4"; VLANId = 10; } ; } ];
+      type = with types; listOf (submodule bridgeMDBOptions);
+      description = lib.mdDoc ''
+        A list of BridgeMDB sections to be added to the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    lldpConfig = mkOption {
+      default = {};
+      example = { MUDURL = "https://things.example.org/product_abc123/v5"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionLLDP;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[LLDP]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    canConfig = mkOption {
+      default = {};
+      example = { };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionCAN;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[CAN]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    ipoIBConfig = mkOption {
+      default = {};
+      example = { };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPoIB;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[IPoIB]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    qdiscConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionQDisc;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[QDisc]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    networkEmulatorConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; DelaySec = "20msec"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionNetworkEmulator;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[NetworkEmulator]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    tokenBucketFilterConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; Rate = "100k"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionTokenBucketFilter;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[TokenBucketFilter]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    pieConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PacketLimit = "3847"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionPIE;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[PIE]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    flowQueuePIEConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PacketLimit = "3847"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionFlowQueuePIE;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[FlowQueuePIE]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    stochasticFairBlueConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PacketLimit = "3847"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionStochasticFairBlue;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[StochasticFairBlue]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    stochasticFairnessQueueingConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PerturbPeriodSec = "30"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionStochasticFairnessQueueing;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[StochasticFairnessQueueing]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    bfifoConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; LimitBytes = "20K"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionBFIFO;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[BFIFO]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    pfifoConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PacketLimit = "300"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionPFIFO;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[PFIFO]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    pfifoHeadDropConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; PacketLimit = "300"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionPFIFOHeadDrop;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[PFIFOHeadDrop]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    pfifoFastConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionPFIFOFast;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[PFIFOFast]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    cakeConfig = mkOption {
+      default = {};
+      example = { Bandwidth = "40M"; OverheadBytes = 8; CompensationMode = "ptm"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionCAKE;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[CAKE]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    controlledDelayConfig = mkOption {
+      default = {};
+      example = { Parent = "ingress"; TargetSec = "20msec"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionControlledDelay;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[ControlledDelay]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    deficitRoundRobinSchedulerConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDeficitRoundRobinScheduler;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[DeficitRoundRobinScheduler]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    deficitRoundRobinSchedulerClassConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; QuantumBytes = "300k"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionDeficitRoundRobinSchedulerClass;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[DeficitRoundRobinSchedulerClass]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    enhancedTransmissionSelectionConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; QuantumBytes = "300k"; Bands = 3; PriorityMap = "100 200 300"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionEnhancedTransmissionSelection;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[EnhancedTransmissionSelection]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    genericRandomEarlyDetectionConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; VirtualQueues = 5; DefaultVirtualQueue = 3; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionGenericRandomEarlyDetection;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[GenericRandomEarlyDetection]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    fairQueueingControlledDelayConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; Flows = 5; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionFairQueueingControlledDelay;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[FairQueueingControlledDelay]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    fairQueueingConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; FlowLimit = 5; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionFairQueueing;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[FairQueueing]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    trivialLinkEqualizerConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; Id = 0; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionTrivialLinkEqualizer;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[TrivialLinkEqualizer]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    hierarchyTokenBucketConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionHierarchyTokenBucket;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[HierarchyTokenBucket]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    hierarchyTokenBucketClassConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; Rate = "10M"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionHierarchyTokenBucketClass;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[HierarchyTokenBucketClass]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    heavyHitterFilterConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; PacketLimit = 10000; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionHeavyHitterFilter;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[HeavyHitterFilter]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    quickFairQueueingConfig = mkOption {
+      default = {};
+      example = { Parent = "root"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionQuickFairQueueing;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[QuickFairQueueing]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    quickFairQueueingConfigClass = mkOption {
+      default = {};
+      example = { Parent = "root"; Weight = 133; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionQuickFairQueueingClass;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[QuickFairQueueingClass]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    bridgeVLANConfig = mkOption {
+      default = {};
+      example = { VLAN = "10-20"; };
+      type = types.addCheck (types.attrsOf unitOption) check.network.sectionBridgeVLAN;
+      description = lib.mdDoc ''
+        Each attribute in this set specifies an option in the
+        `[BridgeVLAN]` section of the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
+    bridgeVLANs = mkOption {
+      default = [];
+      example = [ { bridgeVLANConfig = { VLAN = "10-20"; }; } ];
+      type = with types; listOf (submodule bridgeVLANOptions);
+      description = lib.mdDoc ''
+        A list of BridgeVLAN sections to be added to the unit.  See
+        {manpage}`systemd.network(5)` for details.
+      '';
+    };
+
     name = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -1705,6 +2667,14 @@ let
           [Tap]
           ${attrsToSection def.tapConfig}
         ''
+        + optionalString (def.l2tpConfig != { }) ''
+          [L2TP]
+          ${attrsToSection def.l2tpConfig}
+        ''
+        + flip concatMapStrings def.l2tpSessions (x: ''
+          [L2TPSession]
+          ${attrsToSection x.l2tpSessionConfig}
+        '')
         + optionalString (def.wireguardConfig != { }) ''
           [WireGuard]
           ${attrsToSection def.wireguardConfig}
@@ -1844,19 +2814,145 @@ let
           [DHCPServerStaticLease]
           ${attrsToSection x.dhcpServerStaticLeaseConfig}
         '')
+        + optionalString (def.bridgeConfig != { }) ''
+          [Bridge]
+          ${attrsToSection def.bridgeConfig}
+        ''
+        + flip concatMapStrings def.bridgeFDBs (x: ''
+          [BridgeFDB]
+          ${attrsToSection x.bridgeFDBConfig}
+        '')
+        + flip concatMapStrings def.bridgeMDBs (x: ''
+          [BridgeMDB]
+          ${attrsToSection x.bridgeMDBConfig}
+        '')
+        + optionalString (def.lldpConfig != { }) ''
+          [LLDP]
+          ${attrsToSection def.lldpConfig}
+        ''
+        + optionalString (def.canConfig != { }) ''
+          [CAN]
+          ${attrsToSection def.canConfig}
+        ''
+        + optionalString (def.ipoIBConfig != { }) ''
+          [IPoIB]
+          ${attrsToSection def.ipoIBConfig}
+        ''
+        + optionalString (def.qdiscConfig != { }) ''
+          [QDisc]
+          ${attrsToSection def.qdiscConfig}
+        ''
+        + optionalString (def.networkEmulatorConfig != { }) ''
+          [NetworkEmulator]
+          ${attrsToSection def.networkEmulatorConfig}
+        ''
+        + optionalString (def.tokenBucketFilterConfig != { }) ''
+          [TokenBucketFilter]
+          ${attrsToSection def.tockenBucketFilterConfig}
+        ''
+        + optionalString (def.pieConfig != { }) ''
+          [PIE]
+          ${attrsToSection def.pieConfig}
+        ''
+        + optionalString (def.flowQueuePIEConfig != { }) ''
+          [FlowQueuePIE]
+          ${attrsToSection def.flowQueuePIEConfig}
+        ''
+        + optionalString (def.stochasticFairBlueConfig != { }) ''
+          [StochasticFairBlue]
+          ${attrsToSection def.stochasticFairBlueConfig}
+        ''
+        + optionalString (def.stochasticFairnessQueueingConfig != { }) ''
+          [StochasticFairnessQueueing]
+          ${attrsToSection def.stochasticFairnessQueueingConfig}
+        ''
+        + optionalString (def.bfifoConfig != { }) ''
+          [BFIFO]
+          ${attrsToSection def.bfifoConfig}
+        ''
+        + optionalString (def.pfifoConfig != { }) ''
+          [PFIFO]
+          ${attrsToSection def.pfifoConfig}
+        ''
+        + optionalString (def.pfifoHeadDropConfig != { }) ''
+          [PFIFOHeadDrop]
+          ${attrsToSection def.pfifoHeadDropConfig}
+        ''
+        + optionalString (def.pfifoFastConfig != { }) ''
+          [PFIFOFast]
+          ${attrsToSection def.pfifoFastConfig}
+        ''
+        + optionalString (def.cakeConfig != { }) ''
+          [CAKE]
+          ${attrsToSection def.cakeConfig}
+        ''
+        + optionalString (def.controlledDelayConfig != { }) ''
+          [ControlledDelay]
+          ${attrsToSection def.controlledDelayConfig}
+        ''
+        + optionalString (def.deficitRoundRobinSchedulerConfig != { }) ''
+          [DeficitRoundRobinScheduler]
+          ${attrsToSection def.deficitRoundRobinSchedulerConfig}
+        ''
+        + optionalString (def.deficitRoundRobinSchedulerClassConfig != { }) ''
+          [DeficitRoundRobinSchedulerClass]
+          ${attrsToSection def.deficitRoundRobinSchedulerClassConfig}
+        ''
+        + optionalString (def.enhancedTransmissionSelectionConfig != { }) ''
+          [EnhancedTransmissionSelection]
+          ${attrsToSection def.enhancedTransmissionSelectionConfig}
+        ''
+        + optionalString (def.genericRandomEarlyDetectionConfig != { }) ''
+          [GenericRandomEarlyDetection]
+          ${attrsToSection def.genericRandomEarlyDetectionConfig}
+        ''
+        + optionalString (def.fairQueueingControlledDelayConfig != { }) ''
+          [FairQueueingControlledDelay]
+          ${attrsToSection def.fairQueueingControlledDelayConfig}
+        ''
+        + optionalString (def.fairQueueingConfig != { }) ''
+          [FairQueueing]
+          ${attrsToSection def.fairQueueingConfig}
+        ''
+        + optionalString (def.trivialLinkEqualizerConfig != { }) ''
+          [TrivialLinkEqualizer]
+          ${attrsToSection def.trivialLinkEqualizerConfig}
+        ''
+        + optionalString (def.hierarchyTokenBucketConfig != { }) ''
+          [HierarchyTokenBucket]
+          ${attrsToSection def.hierarchyTokenBucketConfig}
+        ''
+        + optionalString (def.hierarchyTokenBucketClassConfig != { }) ''
+          [HierarchyTokenBucketClass]
+          ${attrsToSection def.hierarchyTokenBucketClassConfig}
+        ''
+        + optionalString (def.heavyHitterFilterConfig != { }) ''
+          [HeavyHitterFilter]
+          ${attrsToSection def.heavyHitterFilterConfig}
+        ''
+        + optionalString (def.quickFairQueueingConfig != { }) ''
+          [QuickFairQueueing]
+          ${attrsToSection def.quickFairQueueingConfig}
+        ''
+        + optionalString (def.quickFairQueueingConfigClass != { }) ''
+          [QuickFairQueueingClass]
+          ${attrsToSection def.quickFairQueueingConfigClass}
+        ''
+        + flip concatMapStrings def.bridgeVLANs (x: ''
+          [BridgeVLAN]
+          ${attrsToSection x.bridgeVLANConfig}
+        '')
         + def.extraConfig;
     };
 
-  unitFiles = listToAttrs (map (name: {
-    name = "systemd/network/${name}";
+  mkUnitFiles = prefix: cfg: listToAttrs (map (name: {
+    name = "${prefix}systemd/network/${name}";
     value.source = "${cfg.units.${name}.unit}/${name}";
   }) (attrNames cfg.units));
-in
 
-{
-  options = {
+  commonOptions = visible: {
 
-    systemd.network.enable = mkOption {
+    enable = mkOption {
       default = false;
       type = types.bool;
       description = lib.mdDoc ''
@@ -1864,31 +2960,35 @@ in
       '';
     };
 
-    systemd.network.links = mkOption {
+    links = mkOption {
       default = {};
+      inherit visible;
       type = with types; attrsOf (submodule [ { options = linkOptions; } ]);
       description = lib.mdDoc "Definition of systemd network links.";
     };
 
-    systemd.network.netdevs = mkOption {
+    netdevs = mkOption {
       default = {};
+      inherit visible;
       type = with types; attrsOf (submodule [ { options = netdevOptions; } ]);
       description = lib.mdDoc "Definition of systemd network devices.";
     };
 
-    systemd.network.networks = mkOption {
+    networks = mkOption {
       default = {};
+      inherit visible;
       type = with types; attrsOf (submodule [ { options = networkOptions; } networkConfig ]);
       description = lib.mdDoc "Definition of systemd networks.";
     };
 
-    systemd.network.config = mkOption {
+    config = mkOption {
       default = {};
+      inherit visible;
       type = with types; submodule [ { options = networkdOptions; } networkdConfig ];
       description = lib.mdDoc "Definition of global systemd network config.";
     };
 
-    systemd.network.units = mkOption {
+    units = mkOption {
       description = lib.mdDoc "Definition of networkd units.";
       default = {};
       internal = true;
@@ -1901,7 +3001,7 @@ in
         }));
     };
 
-    systemd.network.wait-online = {
+    wait-online = {
       enable = mkOption {
         type = types.bool;
         default = true;
@@ -1948,7 +3048,7 @@ in
           Extra command-line arguments to pass to systemd-networkd-wait-online.
           These also affect per-interface `systemd-network-wait-online@` services.
 
-          See [{manpage}`systemd-networkd-wait-online.service(8)`](https://www.freedesktop.org/software/systemd/man/systemd-networkd-wait-online.service.html) for all available options.
+          See {manpage}`systemd-networkd-wait-online.service(8)` for all available options.
         '';
         type = with types; listOf str;
         default = [];
@@ -1957,12 +3057,11 @@ in
 
   };
 
-  config = mkMerge [
+  commonConfig = config: let cfg = config.systemd.network; in mkMerge [
 
     # .link units are honored by udev, no matter if systemd-networkd is enabled or not.
     {
       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}" ]
@@ -1972,14 +3071,6 @@ in
 
     (mkIf config.systemd.network.enable {
 
-      users.users.systemd-network.group = "systemd-network";
-
-      systemd.additionalUpstreamSystemUnits = [
-        "systemd-networkd-wait-online.service"
-        "systemd-networkd.service"
-        "systemd-networkd.socket"
-      ];
-
       systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.netdev" (netdevToUnit n v)) cfg.netdevs
         // mapAttrs' (n: v: nameValuePair "${n}.network" (networkToUnit n v)) cfg.networks;
 
@@ -1988,14 +3079,6 @@ in
       # networkd.
       systemd.sockets.systemd-networkd.wantedBy = [ "sockets.target" ];
 
-      systemd.services.systemd-networkd = {
-        wantedBy = [ "multi-user.target" ];
-        aliases = [ "dbus-org.freedesktop.network1.service" ];
-        restartTriggers = map (x: x.source) (attrValues unitFiles) ++ [
-          config.environment.etc."systemd/networkd.conf".source
-        ];
-      };
-
       systemd.services.systemd-networkd-wait-online = {
         inherit (cfg.wait-online) enable;
         wantedBy = [ "network-online.target" ];
@@ -2017,8 +3100,37 @@ in
         };
       };
 
+    })
+  ];
+
+  stage2Config = let
+    cfg = config.systemd.network;
+    unitFiles = mkUnitFiles "" cfg;
+  in mkMerge [
+    (commonConfig config)
+
+    { environment.etc = unitFiles; }
+
+    (mkIf config.systemd.network.enable {
+
+      users.users.systemd-network.group = "systemd-network";
+
+      systemd.additionalUpstreamSystemUnits = [
+        "systemd-networkd-wait-online.service"
+        "systemd-networkd.service"
+        "systemd-networkd.socket"
+      ];
+
       environment.etc."systemd/networkd.conf" = renderConfig cfg.config;
 
+      systemd.services.systemd-networkd = {
+        wantedBy = [ "multi-user.target" ];
+        restartTriggers = map (x: x.source) (attrValues unitFiles) ++ [
+          config.environment.etc."systemd/networkd.conf".source
+        ];
+        aliases = [ "dbus-org.freedesktop.network1.service" ];
+      };
+
       networking.iproute2 = mkIf (cfg.config.addRouteTablesToIPRoute2 && cfg.config.routeTables != { }) {
         enable = mkDefault true;
         rttablesExtraConfig = ''
@@ -2029,6 +3141,117 @@ in
       };
 
       services.resolved.enable = mkDefault true;
+
+    })
+  ];
+
+  stage1Config = let
+    cfg = config.boot.initrd.systemd.network;
+  in mkMerge [
+    (commonConfig config.boot.initrd)
+
+    {
+      systemd.network.enable = mkDefault config.boot.initrd.network.enable;
+      systemd.contents = mkUnitFiles "/etc/" cfg;
+
+      # 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.
+      systemd.network.units = lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units;
+      systemd.storePaths = ["${config.boot.initrd.systemd.package}/lib/systemd/network/99-default.link"];
+    }
+
+    (mkIf cfg.enable {
+
+      systemd.package = pkgs.systemdStage1Network;
+
+      # For networkctl
+      systemd.dbus.enable = mkDefault true;
+
+      systemd.additionalUpstreamUnits = [
+        "systemd-networkd-wait-online.service"
+        "systemd-networkd.service"
+        "systemd-networkd.socket"
+        "systemd-network-generator.service"
+        "network-online.target"
+        "network-pre.target"
+        "network.target"
+        "nss-lookup.target"
+        "nss-user-lookup.target"
+        "remote-fs-pre.target"
+        "remote-fs.target"
+      ];
+      systemd.users.systemd-network = {};
+      systemd.groups.systemd-network = {};
+
+      systemd.contents."/etc/systemd/networkd.conf" = renderConfig cfg.config;
+
+      systemd.services.systemd-networkd.wantedBy = [ "initrd.target" ];
+      systemd.services.systemd-network-generator.wantedBy = [ "sysinit.target" ];
+
+      systemd.storePaths = [
+        "${config.boot.initrd.systemd.package}/lib/systemd/systemd-networkd"
+        "${config.boot.initrd.systemd.package}/lib/systemd/systemd-networkd-wait-online"
+        "${config.boot.initrd.systemd.package}/lib/systemd/systemd-network-generator"
+      ];
+      kernelModules = [ "af_packet" ];
+
+      systemd.services.nixos-flush-networkd = mkIf config.boot.initrd.network.flushBeforeStage2 {
+        description = "Flush Network Configuration";
+        wantedBy = ["initrd.target"];
+        after = ["systemd-networkd.service" "dbus.socket" "dbus.service"];
+        before = ["shutdown.target" "initrd-switch-root.target"];
+        conflicts = ["shutdown.target" "initrd-switch-root.target"];
+        unitConfig.DefaultDependencies = false;
+        serviceConfig = {
+          # This service does nothing when starting, but brings down
+          # interfaces when switching root. This is the easiest way to
+          # ensure proper ordering while stopping. See systemd.unit(5)
+          # section on Before= and After=. The important part is that
+          # we are stopped before units we need, like dbus.service,
+          # and that we are stopped before starting units like
+          # initrd-switch-root.target
+          Type = "oneshot";
+          RemainAfterExit = true;
+          ExecStart = "/bin/true";
+        };
+        # systemd-networkd doesn't bring down interfaces on its own
+        # when it exits (see: systemd-networkd(8)), so we have to do
+        # it ourselves. The networkctl command doesn't have a way to
+        # bring all interfaces down, so we have to iterate over the
+        # list and filter out unmanaged interfaces to bring them down
+        # individually.
+        preStop = ''
+          networkctl list --full --no-legend | while read _idx link _type _operational setup _; do
+            [ "$setup" = unmanaged ] && continue
+            networkctl down "$link"
+          done
+        '';
+      };
+
+    })
+  ];
+
+in
+
+{
+  options = {
+    systemd.network = commonOptions true;
+    boot.initrd.systemd.network = commonOptions "shallow";
+  };
+
+  config = mkMerge [
+    stage2Config
+    (mkIf config.boot.initrd.systemd.enable {
+      assertions = [{
+        assertion = config.boot.initrd.network.udhcpc.extraArgs == [];
+        message = ''
+          boot.initrd.network.udhcpc.extraArgs is not supported when
+          boot.initrd.systemd.enable is enabled
+        '';
+      }];
+
+      boot.initrd = stage1Config;
     })
   ];
 }
diff --git a/nixos/modules/system/boot/plymouth.nix b/nixos/modules/system/boot/plymouth.nix
index 9b6472fea42..a1ab7093857 100644
--- a/nixos/modules/system/boot/plymouth.nix
+++ b/nixos/modules/system/boot/plymouth.nix
@@ -146,6 +146,9 @@ in
     systemd.services.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
     systemd.paths.systemd-ask-password-plymouth.wantedBy = [ "multi-user.target" ];
 
+    # Prevent Plymouth taking over the screen during system updates.
+    systemd.services.plymouth-start.restartIfChanged = false;
+
     boot.initrd.systemd = {
       extraBin.plymouth = "${plymouth}/bin/plymouth"; # for the recovery shell
       storePaths = [
diff --git a/nixos/modules/system/boot/resolved.nix b/nixos/modules/system/boot/resolved.nix
index 0ab2a875975..4e7201833db 100644
--- a/nixos/modules/system/boot/resolved.nix
+++ b/nixos/modules/system/boot/resolved.nix
@@ -16,7 +16,9 @@ in
       default = false;
       type = types.bool;
       description = lib.mdDoc ''
-        Whether to enable the systemd DNS resolver daemon.
+        Whether to enable the systemd DNS resolver daemon, `systemd-resolved`.
+
+        Search for `services.resolved` to see all options.
       '';
     };
 
diff --git a/nixos/modules/system/boot/stage-1-init.sh b/nixos/modules/system/boot/stage-1-init.sh
index 4596c160a95..835788dbbc9 100644
--- a/nixos/modules/system/boot/stage-1-init.sh
+++ b/nixos/modules/system/boot/stage-1-init.sh
@@ -73,7 +73,7 @@ trap 'fail' 0
 
 # Print a greeting.
 info
-info "<<< NixOS Stage 1 >>>"
+info "<<< @distroName@ Stage 1 >>>"
 info
 
 # Make several required directories.
@@ -234,8 +234,7 @@ done
 mkdir -p /lib
 ln -s @modulesClosure@/lib/modules /lib/modules
 ln -s @modulesClosure@/lib/firmware /lib/firmware
-# see comment in stage-1.nix for explanation
-echo @extraUtils@/bin/modprobe-kernel > /proc/sys/kernel/modprobe
+echo @extraUtils@/bin/modprobe > /proc/sys/kernel/modprobe
 for i in @kernelModules@; do
     info "loading module $(basename $i)..."
     modprobe $i
@@ -411,6 +410,11 @@ mountFS() {
         n=$((n + 1))
     done
 
+    # For bind mounts, busybox has a tendency to ignore options, which can be a
+    # security issue (e.g. "nosuid"). Remounting the partition seems to fix the
+    # issue.
+    mount "/mnt-root$mountPoint" -o "remount,$optionsPrefixed"
+
     [ "$mountPoint" == "/" ] &&
         [ -f "/mnt-root/etc/NIXOS_LUSTRATE" ] &&
         lustrateRoot "/mnt-root"
@@ -422,7 +426,7 @@ lustrateRoot () {
     local root="$1"
 
     echo
-    echo -e "\e[1;33m<<< NixOS is now lustrating the root filesystem (cruft goes to /old-root) >>>\e[0m"
+    echo -e "\e[1;33m<<< @distroName@ is now lustrating the root filesystem (cruft goes to /old-root) >>>\e[0m"
     echo
 
     mkdir -m 0755 -p "$root/old-root.tmp"
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index 95dcdfd7fbe..1229f635752 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -90,7 +90,7 @@ let
   # copy what we need.  Instead of using statically linked binaries,
   # we just copy what we need from Glibc and use patchelf to make it
   # work.
-  extraUtils = pkgs.runCommandCC "extra-utils"
+  extraUtils = pkgs.runCommand "extra-utils"
     { nativeBuildInputs = [pkgs.buildPackages.nukeReferences];
       allowedReferences = [ "out" ]; # prevent accidents like glibc being included in the initrd
     }
@@ -150,26 +150,6 @@ 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.
@@ -342,6 +322,8 @@ let
 
     inherit (config.boot) resumeDevice;
 
+    inherit (config.system.nixos) distroName;
+
     inherit (config.system.build) earlyMountScript;
 
     inherit (config.boot.initrd) checkJournalingFS verbose
@@ -463,7 +445,8 @@ let
           ) config.boot.initrd.secrets)
          }
 
-        (cd "$tmp" && find . -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \
+        # mindepth 1 so that we don't change the mode of /
+        (cd "$tmp" && find . -mindepth 1 -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \
           ${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1"
       '';
 
diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh
index 78cc8e8d45a..f9a2084ea9e 100755
--- a/nixos/modules/system/boot/stage-2-init.sh
+++ b/nixos/modules/system/boot/stage-2-init.sh
@@ -19,7 +19,7 @@ if [ "${IN_NIXOS_SYSTEMD_STAGE1:-}" != true ]; then
 
     # Print a greeting.
     echo
-    echo -e "\e[1;32m<<< NixOS Stage 2 >>>\e[0m"
+    echo -e "\e[1;32m<<< @distroName@ Stage 2 >>>\e[0m"
     echo
 
 
diff --git a/nixos/modules/system/boot/stage-2.nix b/nixos/modules/system/boot/stage-2.nix
index 6ed915c339e..001380158d5 100644
--- a/nixos/modules/system/boot/stage-2.nix
+++ b/nixos/modules/system/boot/stage-2.nix
@@ -11,6 +11,7 @@ let
     shellDebug = "${pkgs.bashInteractive}/bin/bash";
     shell = "${pkgs.bash}/bin/bash";
     inherit (config.boot) readOnlyNixStore systemdExecutable extraSystemdUnitPaths;
+    inherit (config.system.nixos) distroName;
     isExecutable = true;
     inherit useHostResolvConf;
     inherit (config.system.build) earlyMountScript;
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index e37ed853181..5ff99b14dee 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -79,6 +79,8 @@ let
       # Filesystems.
       "systemd-fsck@.service"
       "systemd-fsck-root.service"
+      "systemd-growfs@.service"
+      "systemd-growfs-root.service"
       "systemd-remount-fs.service"
       "systemd-pstore.service"
       "local-fs.target"
@@ -450,7 +452,7 @@ in
         (mkAfter [ "systemd" ])
       ]);
       group = (mkMerge [
-        (mkAfter [ "systemd" ])
+        (mkAfter [ "[success=merge] systemd" ]) # need merge so that NSS won't stop at file-based groups
       ]);
     };
 
@@ -614,7 +616,7 @@ in
 
     # Avoid potentially degraded system state due to
     # "Userspace Out-Of-Memory (OOM) Killer was skipped because of a failed condition check (ConditionControlGroupController=v2)."
-    systemd.services.systemd-oomd.enable = mkIf (!cfg.enableUnifiedCgroupHierarchy) false;
+    systemd.oomd.enable = mkIf (!cfg.enableUnifiedCgroupHierarchy) false;
 
     services.logrotate.settings = {
       "/var/log/btmp" = mapAttrs (_: mkDefault) {
diff --git a/nixos/modules/system/boot/systemd/coredump.nix b/nixos/modules/system/boot/systemd/coredump.nix
index c2ca973d380..03ef00e5683 100644
--- a/nixos/modules/system/boot/systemd/coredump.nix
+++ b/nixos/modules/system/boot/systemd/coredump.nix
@@ -44,7 +44,21 @@ in {
         '';
 
         # install provided sysctl snippets
-        "sysctl.d/50-coredump.conf".source = "${systemd}/example/sysctl.d/50-coredump.conf";
+        "sysctl.d/50-coredump.conf".source =
+          # Fix systemd-coredump error caused by truncation of `kernel.core_pattern`
+          # when the `systemd` derivation name is too long. This works by substituting
+          # the path to `systemd` with a symlink that has a constant-length path.
+          #
+          # See: https://github.com/NixOS/nixpkgs/issues/213408
+          pkgs.substitute {
+            src = "${systemd}/example/sysctl.d/50-coredump.conf";
+            replacements = [
+              "--replace"
+              "${systemd}"
+              "${pkgs.symlinkJoin { name = "systemd"; paths = [ systemd ]; }}"
+            ];
+          };
+
         "sysctl.d/50-default.conf".source = "${systemd}/example/sysctl.d/50-default.conf";
       };
 
diff --git a/nixos/modules/system/boot/systemd/homed.nix b/nixos/modules/system/boot/systemd/homed.nix
new file mode 100644
index 00000000000..403d1690124
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/homed.nix
@@ -0,0 +1,43 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.homed;
+in
+{
+  options.services.homed.enable = lib.mkEnableOption (lib.mdDoc ''
+    Enable systemd home area/user account manager
+  '');
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = config.services.nscd.enable;
+        message = "systemd-homed requires the use of systemd nss module. services.nscd.enable must be set to true,";
+      }
+    ];
+
+    systemd.additionalUpstreamSystemUnits = [
+      "systemd-homed.service"
+      "systemd-homed-activate.service"
+    ];
+
+    # This is mentioned in homed's [Install] section.
+    #
+    # While homed appears to work without it, it's probably better
+    # to follow upstream recommendations.
+    services.userdbd.enable = lib.mkDefault true;
+
+    systemd.services = {
+      systemd-homed = {
+        # These packages are required to manage encrypted volumes
+        path = config.system.fsPackages;
+        aliases = [ "dbus-org.freedesktop.home1.service" ];
+        wantedBy = [ "multi-user.target" ];
+      };
+
+      systemd-homed-activate = {
+        wantedBy = [ "systemd-homed.service" ];
+      };
+    };
+  };
+}
diff --git a/nixos/modules/system/boot/systemd/initrd-secrets.nix b/nixos/modules/system/boot/systemd/initrd-secrets.nix
index bc65880719d..7b59c0cbe7b 100644
--- a/nixos/modules/system/boot/systemd/initrd-secrets.nix
+++ b/nixos/modules/system/boot/systemd/initrd-secrets.nix
@@ -19,13 +19,13 @@
       # 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
+        for secret in $(cd /.initrd-secrets; find . -type f -o -type l); do
           mkdir -p "$(dirname "/$secret")"
           cp "/.initrd-secrets/$secret" "/$secret"
         done
       '';
 
-      unitConfig = {
+      serviceConfig = {
         Type = "oneshot";
         RemainAfterExit = true;
       };
diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix
index 31702499b0f..c9c219d0a0a 100644
--- a/nixos/modules/system/boot/systemd/initrd.nix
+++ b/nixos/modules/system/boot/systemd/initrd.nix
@@ -1,4 +1,4 @@
-{ lib, config, utils, pkgs, ... }:
+{ lib, options, config, utils, pkgs, ... }:
 
 with lib;
 
@@ -56,6 +56,7 @@ let
     "systemd-ask-password-console.path"
     "systemd-ask-password-console.service"
     "systemd-fsck@.service"
+    "systemd-growfs@.service"
     "systemd-halt.service"
     "systemd-hibernate-resume@.service"
     "systemd-journald-audit.socket"
@@ -71,15 +72,6 @@ let
     "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 = [
@@ -118,7 +110,7 @@ let
     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);
+    postBuild = concatStringsSep "\n" (mapAttrsToList (n: v: "ln -sf '${v}' $out/bin/'${n}'") cfg.extraBin);
   };
 
   initialRamdisk = pkgs.makeInitrdNG {
@@ -134,18 +126,40 @@ in {
   options.boot.initrd.systemd = {
     enable = mkEnableOption (lib.mdDoc "systemd in initrd") // {
       description = lib.mdDoc ''
-        Whether to enable 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.
+        Whether to enable systemd in initrd. The unit options such as
+        {option}`boot.initrd.systemd.services` are the same as their
+        stage 2 counterparts such as {option}`systemd.services`,
+        except that `restartTriggers` and `reloadTriggers` are not
+        supported.
+
+        Note: This is experimental. Some of the `boot.initrd` options
+        are not supported when this is enabled, and the options under
+        `boot.initrd.systemd` are subject to change.
       '';
     };
 
-    package = (mkPackageOption pkgs "systemd" {
+    package = mkPackageOptionMD pkgs "systemd" {
       default = "systemdStage1";
-    }) // {
-      visible = false;
+    };
+
+    extraConfig = mkOption {
+      default = "";
+      type = types.lines;
+      example = "DefaultLimitCORE=infinity";
+      description = lib.mdDoc ''
+        Extra config options for systemd. See systemd-system.conf(5) man page
+        for available options.
+      '';
+    };
+
+    managerEnvironment = mkOption {
+      type = with types; attrsOf (nullOr (oneOf [ str path package ]));
+      default = {};
+      example = { SYSTEMD_LOG_LEVEL = "debug"; };
+      description = lib.mdDoc ''
+        Environment variables of PID 1. These variables are
+        *not* passed to started units.
+      '';
     };
 
     contents = mkOption {
@@ -155,7 +169,6 @@ in {
           "/etc/hostname".text = "mymachine";
         }
       '';
-      visible = false;
       default = {};
       type = utils.systemdUtils.types.initrdContents;
     };
@@ -205,7 +218,6 @@ in {
 
     emergencyAccess = mkOption {
       type = with types; oneOf [ bool (nullOr (passwdEntry str)) ];
-      visible = false;
       description = lib.mdDoc ''
         Set to true for unauthenticated emergency access, and false for
         no emergency access.
@@ -219,7 +231,6 @@ in {
     initrdBin = mkOption {
       type = types.listOf types.package;
       default = [];
-      visible = false;
       description = lib.mdDoc ''
         Packages to include in /bin for the stage 1 emergency shell.
       '';
@@ -228,7 +239,6 @@ in {
     additionalUpstreamUnits = mkOption {
       default = [ ];
       type = types.listOf types.str;
-      visible = false;
       example = [ "debug-shell.service" "systemd-quotacheck.service" ];
       description = lib.mdDoc ''
         Additional units shipped with systemd that shall be enabled.
@@ -239,7 +249,6 @@ in {
       default = [ ];
       type = types.listOf types.str;
       example = [ "systemd-backlight@.service" ];
-      visible = false;
       description = lib.mdDoc ''
         A list of units to skip when generating system systemd configuration directory. This has
         priority over upstream units, {option}`boot.initrd.systemd.units`, and
@@ -252,13 +261,12 @@ in {
     units = mkOption {
       description = lib.mdDoc "Definition of systemd units.";
       default = {};
-      visible = false;
+      visible = "shallow";
       type = systemdUtils.types.units;
     };
 
     packages = mkOption {
       default = [];
-      visible = false;
       type = types.listOf types.package;
       example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]";
       description = lib.mdDoc "Packages providing systemd units and hooks.";
@@ -266,7 +274,7 @@ in {
 
     targets = mkOption {
       default = {};
-      visible = false;
+      visible = "shallow";
       type = systemdUtils.types.initrdTargets;
       description = lib.mdDoc "Definition of systemd target units.";
     };
@@ -274,35 +282,35 @@ in {
     services = mkOption {
       default = {};
       type = systemdUtils.types.initrdServices;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc "Definition of systemd service units.";
     };
 
     sockets = mkOption {
       default = {};
       type = systemdUtils.types.initrdSockets;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc "Definition of systemd socket units.";
     };
 
     timers = mkOption {
       default = {};
       type = systemdUtils.types.initrdTimers;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc "Definition of systemd timer units.";
     };
 
     paths = mkOption {
       default = {};
       type = systemdUtils.types.initrdPaths;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc "Definition of systemd path units.";
     };
 
     mounts = mkOption {
       default = [];
       type = systemdUtils.types.initrdMounts;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc ''
         Definition of systemd mount units.
         This is a list instead of an attrSet, because systemd mandates the names to be derived from
@@ -313,7 +321,7 @@ in {
     automounts = mkOption {
       default = [];
       type = systemdUtils.types.automounts;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc ''
         Definition of systemd automount units.
         This is a list instead of an attrSet, because systemd mandates the names to be derived from
@@ -324,7 +332,7 @@ in {
     slices = mkOption {
       default = {};
       type = systemdUtils.types.slices;
-      visible = false;
+      visible = "shallow";
       description = lib.mdDoc "Definition of slice configurations.";
     };
   };
@@ -333,9 +341,11 @@ in {
     system.build = { inherit initialRamdisk; };
 
     boot.initrd.availableKernelModules = [
-      "autofs4"           # systemd needs this for some features
-      "tpm-tis" "tpm-crb" # systemd-cryptenroll
-    ];
+      # systemd needs this for some features
+      "autofs4"
+      # systemd-cryptenroll
+      "tpm-tis"
+    ] ++ lib.optional (pkgs.stdenv.hostPlatform.system != "riscv64-linux") "tpm-crb";
 
     boot.initrd.systemd = {
       initrdBin = [pkgs.bash pkgs.coreutils cfg.package.kmod cfg.package] ++ config.system.fsPackages;
@@ -343,15 +353,21 @@ in {
         less = "${pkgs.less}/bin/less";
         mount = "${cfg.package.util-linux}/bin/mount";
         umount = "${cfg.package.util-linux}/bin/umount";
+        fsck = "${cfg.package.util-linux}/bin/fsck";
       };
 
+      managerEnvironment.PATH = "/bin:/sbin";
+
       contents = {
+        "/tmp/.keep".text = "systemd requires the /tmp mount point in the initrd cpio archive";
         "/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"}
+          DefaultEnvironment=PATH=/bin:/sbin
+          ${cfg.extraConfig}
+          ManagerEnvironment=${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}=${lib.escapeShellArg v}") cfg.managerEnvironment)}
         '';
 
         "/lib/modules".source = "${modulesClosure}/lib/modules";
@@ -359,8 +375,10 @@ in {
 
         "/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}:::::::";
+        # We can use either ! or * to lock the root account in the
+        # console, but some software like OpenSSH won't even allow you
+        # to log in with an SSH key if you use ! so we use * instead
+        "/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then optionalString (!cfg.emergencyAccess) "*" else cfg.emergencyAccess}:::::::";
 
         "/bin".source = "${initrdBinEnv}/bin";
         "/sbin".source = "${initrdBinEnv}/sbin";
@@ -414,9 +432,6 @@ in {
         # fido2 support
         "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-fido2.so"
         "${pkgs.libfido2}/lib/libfido2.so.1"
-
-        # the unwrapped systemd-cryptsetup executable
-        "${cfg.package}/lib/systemd/.systemd-cryptsetup-wrapped"
       ] ++ jobScripts;
 
       targets.initrd.aliases = ["default.target"];
@@ -434,21 +449,6 @@ in {
                      (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; };
-
       # make sure all the /dev nodes are set up
       services.systemd-tmpfiles-setup-dev.wantedBy = ["sysinit.target"];
 
@@ -482,7 +482,7 @@ in {
 
           # 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
+          if ! [ -x "/sysroot$(readlink "/sysroot$closure/prepare-root" || echo "$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
diff --git a/nixos/modules/system/boot/systemd/repart.nix b/nixos/modules/system/boot/systemd/repart.nix
new file mode 100644
index 00000000000..8f3a7002377
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/repart.nix
@@ -0,0 +1,123 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.systemd.repart;
+  initrdCfg = config.boot.initrd.systemd.repart;
+
+  writeDefinition = name: partitionConfig: pkgs.writeText
+    "${name}.conf"
+    (lib.generators.toINI { } { Partition = partitionConfig; });
+
+  listOfDefinitions = lib.mapAttrsToList
+    writeDefinition
+    (lib.filterAttrs (k: _: !(lib.hasPrefix "_" k)) cfg.partitions);
+
+  # Create a directory in the store that contains a copy of all definition
+  # files. This is then passed to systemd-repart in the initrd so it can access
+  # the definition files after the sysroot has been mounted but before
+  # activation. This needs a hard copy of the files and not just symlinks
+  # because otherwise the files do not show up in the sysroot.
+  definitionsDirectory = pkgs.runCommand "systemd-repart-definitions" { } ''
+    mkdir -p $out
+    ${(lib.concatStringsSep "\n"
+      (map (pkg: "cp ${pkg} $out/${pkg.name}") listOfDefinitions)
+    )}
+  '';
+in
+{
+  options = {
+    boot.initrd.systemd.repart.enable = lib.mkEnableOption (lib.mdDoc "systemd-repart") // {
+      description = lib.mdDoc ''
+        Grow and add partitions to a partition table at boot time in the initrd.
+        systemd-repart only works with GPT partition tables.
+
+        To run systemd-repart after the initrd, see
+        `options.systemd.repart.enable`.
+      '';
+    };
+
+    systemd.repart = {
+      enable = lib.mkEnableOption (lib.mdDoc "systemd-repart") // {
+        description = lib.mdDoc ''
+          Grow and add partitions to a partition table.
+          systemd-repart only works with GPT partition tables.
+
+          To run systemd-repart while in the initrd, see
+          `options.boot.initrd.systemd.repart.enable`.
+        '';
+      };
+
+      partitions = lib.mkOption {
+        type = with lib.types; attrsOf (attrsOf (oneOf [ str int bool ]));
+        default = { };
+        example = {
+          "10-root" = {
+            Type = "root";
+          };
+          "20-home" = {
+            Type = "home";
+            SizeMinBytes = "512M";
+            SizeMaxBytes = "2G";
+          };
+        };
+        description = lib.mdDoc ''
+          Specify partitions as a set of the names of the definition files as the
+          key and the partition configuration as its value. The partition
+          configuration can use all upstream options. See <link
+          xlink:href="https://www.freedesktop.org/software/systemd/man/repart.d.html"/>
+          for all available options.
+        '';
+      };
+    };
+  };
+
+  config = lib.mkIf (cfg.enable || initrdCfg.enable) {
+    # Always link the definitions into /etc so that they are also included in
+    # the /nix/store of the sysroot during early userspace (i.e. while in the
+    # initrd).
+    environment.etc."repart.d".source = definitionsDirectory;
+
+    boot.initrd.systemd = lib.mkIf initrdCfg.enable {
+      additionalUpstreamUnits = [
+        "systemd-repart.service"
+      ];
+
+      storePaths = [
+        "${config.boot.initrd.systemd.package}/bin/systemd-repart"
+      ];
+
+      # Override defaults in upstream unit.
+      services.systemd-repart = {
+        # Unset the conditions as they cannot be met before activation because
+        # the definition files are not stored in the expected locations.
+        unitConfig.ConditionDirectoryNotEmpty = [
+          " " # required to unset the previous value.
+        ];
+        serviceConfig = {
+          # systemd-repart runs before the activation script. Thus we cannot
+          # rely on them being linked in /etc already. Instead we have to
+          # explicitly pass their location in the sysroot to the binary.
+          ExecStart = [
+            " " # required to unset the previous value.
+            ''${config.boot.initrd.systemd.package}/bin/systemd-repart \
+                  --definitions=/sysroot${definitionsDirectory} \
+                  --dry-run=no
+            ''
+          ];
+        };
+        # Because the initrd does not have the `initrd-usr-fs.target` the
+        # upestream unit runs too early in the boot process, before the sysroot
+        # is available. However, systemd-repart needs access to the sysroot to
+        # find the definition files.
+        after = [ "sysroot.mount" ];
+      };
+    };
+
+    systemd = lib.mkIf cfg.enable {
+      additionalUpstreamSystemUnits = [
+        "systemd-repart.service"
+      ];
+    };
+  };
+
+}
diff --git a/nixos/modules/system/boot/systemd/user.nix b/nixos/modules/system/boot/systemd/user.nix
index 46d66fe4e68..92e1b087392 100644
--- a/nixos/modules/system/boot/systemd/user.nix
+++ b/nixos/modules/system/boot/systemd/user.nix
@@ -39,6 +39,20 @@ let
     "timers.target"
     "xdg-desktop-autostart.target"
   ] ++ config.systemd.additionalUpstreamUserUnits;
+
+  writeTmpfiles = { rules, user ? null }:
+    let
+      suffix = if user == null then "" else "-${user}";
+    in
+    pkgs.writeTextFile {
+      name = "nixos-user-tmpfiles.d${suffix}";
+      destination = "/etc/xdg/user-tmpfiles.d/00-nixos${suffix}.conf";
+      text = ''
+        # This file is created automatically and should not be modified.
+        # Please change the options ‘systemd.user.tmpfiles’ instead.
+        ${concatStringsSep "\n" rules}
+      '';
+    };
 in {
   options = {
     systemd.user.extraConfig = mkOption {
@@ -46,7 +60,7 @@ in {
       type = types.lines;
       example = "DefaultCPUAccounting=yes";
       description = lib.mdDoc ''
-        Extra config options for systemd user instances. See man systemd-user.conf for
+        Extra config options for systemd user instances. See {manpage}`systemd-user.conf(5)` for
         available options.
       '';
     };
@@ -93,6 +107,43 @@ in {
       description = lib.mdDoc "Definition of systemd per-user timer units.";
     };
 
+    systemd.user.tmpfiles = {
+      rules = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "D %C - - - 7d" ];
+        description = lib.mdDoc ''
+          Global user rules for creation, deletion and cleaning of volatile and
+          temporary files automatically. See
+          {manpage}`tmpfiles.d(5)`
+          for the exact format.
+        '';
+      };
+
+      users = mkOption {
+        description = mdDoc ''
+          Per-user rules for creation, deletion and cleaning of volatile and
+          temporary files automatically.
+        '';
+        default = {};
+        type = types.attrsOf (types.submodule {
+          options = {
+            rules = mkOption {
+              type = types.listOf types.str;
+              default = [];
+              example = [ "D %C - - - 7d" ];
+              description = mdDoc ''
+                Per-user rules for creation, deletion and cleaning of volatile and
+                temporary files automatically. See
+                {manpage}`tmpfiles.d(5)`
+                for the exact format.
+              '';
+            };
+          };
+        });
+      };
+    };
+
     systemd.additionalUpstreamUserUnits = mkOption {
       default = [];
       type = types.listOf types.str;
@@ -154,5 +205,30 @@ in {
     # Some overrides to upstream units.
     systemd.services."user@".restartIfChanged = false;
     systemd.services.systemd-user-sessions.restartIfChanged = false; # Restart kills all active sessions.
+
+    # enable systemd user tmpfiles
+    systemd.user.services.systemd-tmpfiles-setup.wantedBy =
+      optional
+        (cfg.tmpfiles.rules != [] || any (cfg': cfg'.rules != []) (attrValues cfg.tmpfiles.users))
+        "basic.target";
+
+    # /run/current-system/sw/etc/xdg is in systemd's $XDG_CONFIG_DIRS so we can
+    # write the tmpfiles.d rules for everyone there
+    environment.systemPackages =
+      optional
+        (cfg.tmpfiles.rules != [])
+        (writeTmpfiles { inherit (cfg.tmpfiles) rules; });
+
+    # /etc/profiles/per-user/$USER/etc/xdg is in systemd's $XDG_CONFIG_DIRS so
+    # we can write a single user's tmpfiles.d rules there
+    users.users =
+      mapAttrs
+        (user: cfg': {
+          packages = optional (cfg'.rules != []) (writeTmpfiles {
+            inherit (cfg') rules;
+            inherit user;
+          });
+        })
+        cfg.tmpfiles.users;
   };
 }
diff --git a/nixos/modules/system/boot/systemd/userdbd.nix b/nixos/modules/system/boot/systemd/userdbd.nix
new file mode 100644
index 00000000000..994aa3ca3b8
--- /dev/null
+++ b/nixos/modules/system/boot/systemd/userdbd.nix
@@ -0,0 +1,18 @@
+{ config, lib, ... }:
+
+let
+  cfg = config.services.userdbd;
+in
+{
+  options.services.userdbd.enable = lib.mkEnableOption (lib.mdDoc ''
+    Enables the systemd JSON user/group record lookup service
+  '');
+  config = lib.mkIf cfg.enable {
+    systemd.additionalUpstreamSystemUnits = [
+      "systemd-userdbd.socket"
+      "systemd-userdbd.service"
+    ];
+
+    systemd.sockets.systemd-userdbd.wantedBy = [ "sockets.target" ];
+  };
+}
diff --git a/nixos/modules/system/boot/tmp.nix b/nixos/modules/system/boot/tmp.nix
index 1f9431710ae..fd16cd3fba4 100644
--- a/nixos/modules/system/boot/tmp.nix
+++ b/nixos/modules/system/boot/tmp.nix
@@ -3,62 +3,67 @@
 with lib;
 
 let
-  cfg = config.boot;
+  cfg = config.boot.tmp;
 in
 {
-
-  ###### interface
+  imports = [
+    (mkRenamedOptionModule [ "boot" "cleanTmpDir" ] [ "boot" "tmp" "cleanOnBoot" ])
+    (mkRenamedOptionModule [ "boot" "tmpOnTmpfs" ] [ "boot" "tmp" "useTmpfs" ])
+    (mkRenamedOptionModule [ "boot" "tmpOnTmpfsSize" ] [ "boot" "tmp" "tmpfsSize" ])
+  ];
 
   options = {
+    boot.tmp = {
+      cleanOnBoot = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Whether to delete all files in {file}`/tmp` during boot.
+        '';
+      };
 
-    boot.cleanTmpDir = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-        Whether to delete all files in {file}`/tmp` during boot.
-      '';
-    };
+      tmpfsSize = mkOption {
+        type = types.oneOf [ types.str types.types.ints.positive ];
+        default = "50%";
+        description = lib.mdDoc ''
+          Size of tmpfs in percentage.
+          Percentage is defined by systemd.
+        '';
+      };
 
-    boot.tmpOnTmpfs = mkOption {
-      type = types.bool;
-      default = false;
-      description = lib.mdDoc ''
-         Whether to mount a tmpfs on {file}`/tmp` during boot.
-      '';
-    };
+      useTmpfs = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+           Whether to mount a tmpfs on {file}`/tmp` during boot.
 
-    boot.tmpOnTmpfsSize = mkOption {
-      type = types.oneOf [ types.str types.types.ints.positive ];
-      default = "50%";
-      description = lib.mdDoc ''
-        Size of tmpfs in percentage.
-        Percentage is defined by systemd.
-      '';
+           ::: {.note}
+           Large Nix builds can fail if the mounted tmpfs is not large enough.
+           In such a case either increase the tmpfsSize or disable this option.
+           :::
+        '';
+      };
     };
-
   };
 
-  ###### implementation
-
   config = {
-
     # When changing remember to update /tmp mount in virtualisation/qemu-vm.nix
-    systemd.mounts = mkIf cfg.tmpOnTmpfs [
+    systemd.mounts = mkIf cfg.useTmpfs [
       {
         what = "tmpfs";
         where = "/tmp";
         type = "tmpfs";
-        mountConfig.Options = concatStringsSep "," [ "mode=1777"
-                                                     "strictatime"
-                                                     "rw"
-                                                     "nosuid"
-                                                     "nodev"
-                                                     "size=${toString cfg.tmpOnTmpfsSize}" ];
+        mountConfig.Options = concatStringsSep "," [
+          "mode=1777"
+          "strictatime"
+          "rw"
+          "nosuid"
+          "nodev"
+          "size=${toString cfg.tmpfsSize}"
+        ];
       }
     ];
 
-    systemd.tmpfiles.rules = optional config.boot.cleanTmpDir "D! /tmp 1777 root root";
-
+    systemd.tmpfiles.rules = optional cfg.cleanOnBoot "D! /tmp 1777 root root";
   };
-
 }
diff --git a/nixos/modules/system/etc/setup-etc.pl b/nixos/modules/system/etc/setup-etc.pl
index a048261a3df..ea0a3830817 100644
--- a/nixos/modules/system/etc/setup-etc.pl
+++ b/nixos/modules/system/etc/setup-etc.pl
@@ -13,8 +13,12 @@ sub atomicSymlink {
     my $tmp = "$target.tmp";
     unlink $tmp;
     symlink $source, $tmp or return 0;
-    rename $tmp, $target or return 0;
-    return 1;
+    if (rename $tmp, $target) {
+        return 1;
+    } else {
+        unlink $tmp;
+        return 0;
+    }
 }
 
 
@@ -87,6 +91,12 @@ my @copied;
 
 sub link {
     my $fn = substr $File::Find::name, length($etc) + 1 or next;
+
+    # nixos-enter sets up /etc/resolv.conf as a bind mount, so skip it.
+    if ($fn eq "resolv.conf" and $ENV{'IN_NIXOS_ENTER'}) {
+        return;
+    }
+
     my $target = "/etc/$fn";
     File::Path::make_path(dirname $target);
     $created{$fn} = 1;
@@ -103,7 +113,7 @@ sub link {
     if (-e "$_.mode") {
         my $mode = read_file("$_.mode"); chomp $mode;
         if ($mode eq "direct-symlink") {
-            atomicSymlink readlink("$static/$fn"), $target or warn;
+            atomicSymlink readlink("$static/$fn"), $target or warn "could not create symlink $target";
         } else {
             my $uid = read_file("$_.uid"); chomp $uid;
             my $gid = read_file("$_.gid"); chomp $gid;
@@ -112,12 +122,15 @@ sub link {
             $gid = getgrnam $gid unless $gid =~ /^\+/;
             chown int($uid), int($gid), "$target.tmp" or warn;
             chmod oct($mode), "$target.tmp" or warn;
-            rename "$target.tmp", $target or warn;
+            unless (rename "$target.tmp", $target) {
+                warn "could not create target $target";
+                unlink "$target.tmp";
+            }
         }
         push @copied, $fn;
         print CLEAN "$fn\n";
     } elsif (-l "$_") {
-        atomicSymlink "$static/$fn", $target or warn;
+        atomicSymlink "$static/$fn", $target or warn "could not create symlink $target";
     }
 }
 
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
index a093baea6a6..326862f836a 100644
--- a/nixos/modules/tasks/filesystems.nix
+++ b/nixos/modules/tasks/filesystems.nix
@@ -54,7 +54,7 @@ let
         default = [ "defaults" ];
         example = [ "data=journal" ];
         description = lib.mdDoc "Options used to mount the file system.";
-        type = types.listOf nonEmptyStr;
+        type = types.nonEmptyListOf nonEmptyStr;
       };
 
       depends = mkOption {
@@ -140,7 +140,10 @@ let
         else if config.fsType == "reiserfs" then "-q"
         else null;
     in {
-      options = mkIf config.autoResize [ "x-nixos.autoresize" ];
+      options = mkMerge [
+        (mkIf config.autoResize [ "x-nixos.autoresize" ])
+        (mkIf (utils.fsNeededForBoot config) [ "x-initrd.mount" ])
+      ];
       formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions);
     };
 
@@ -155,27 +158,54 @@ let
 
   makeFstabEntries =
     let
-      fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "nfs4" "vboxsf" "glusterfs" "apfs" "9p" "cifs" "prl_fs" "vmhgfs" ];
+      fsToSkipCheck = [
+        "none"
+        "auto"
+        "overlay"
+        "iso9660"
+        "bindfs"
+        "udf"
+        "btrfs"
+        "zfs"
+        "tmpfs"
+        "bcachefs"
+        "nfs"
+        "nfs4"
+        "nilfs2"
+        "vboxsf"
+        "squashfs"
+        "glusterfs"
+        "apfs"
+        "9p"
+        "cifs"
+        "prl_fs"
+        "vmhgfs"
+      ] ++ lib.optionals (!config.boot.initrd.checkJournalingFS) [
+        "ext3"
+        "ext4"
+        "reiserfs"
+        "xfs"
+        "jfs"
+        "f2fs"
+      ];
       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;
-    in fstabFileSystems: { rootPrefix ? "", excludeChecks ? false, extraOpts ? (fs: []) }: concatMapStrings (fs:
+    in fstabFileSystems: { rootPrefix ? "", extraOpts ? (fs: []) }: concatMapStrings (fs:
       (optionalString (isBindMount fs) (escape rootPrefix))
       + (if fs.device != null then escape fs.device
          else if fs.label != null then "/dev/disk/by-label/${escape fs.label}"
          else throw "No device specified for mount point ‘${fs.mountPoint}’.")
-      + " " + escape (rootPrefix + fs.mountPoint)
+      + " " + escape fs.mountPoint
       + " " + fs.fsType
       + " " + escape (builtins.concatStringsSep "," (fs.options ++ (extraOpts fs)))
-      + " " + (optionalString (!excludeChecks)
-        ("0 " + (if skipCheck fs then "0" else if fs.mountPoint == "/" then "1" else "2")))
+      + " 0 " + (if skipCheck fs then "0" else if fs.mountPoint == "/" then "1" else "2")
       + "\n"
     ) fstabFileSystems;
 
     initrdFstab = pkgs.writeText "initrd-fstab" (makeFstabEntries (filter utils.fsNeededForBoot fileSystems) {
       rootPrefix = "/sysroot";
-      excludeChecks = true;
       extraOpts = fs:
         (optional fs.autoResize "x-systemd.growfs")
         ++ (optional fs.autoFormat "x-systemd.makefs");
@@ -289,7 +319,7 @@ in
         message = let
           fs = head (filter notAutoResizable fileSystems);
         in
-          "Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = \"${fs.fsType}\"':${if fs.fsType == "auto" then " fsType has to be explicitly set and" else ""} only the ext filesystems and f2fs support it.";
+          "Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = \"${fs.fsType}\"':${optionalString (fs.fsType == "auto") " fsType has to be explicitly set and"} only the ext filesystems and f2fs support it.";
       }
     ];
 
@@ -328,7 +358,9 @@ in
         )}
       '';
 
-    boot.initrd.systemd.contents."/etc/fstab".source = initrdFstab;
+    boot.initrd.systemd.storePaths = [initrdFstab];
+    boot.initrd.systemd.managerEnvironment.SYSTEMD_SYSROOT_FSTAB = initrdFstab;
+    boot.initrd.systemd.services.initrd-parse-etc.environment.SYSTEMD_SYSROOT_FSTAB = initrdFstab;
 
     # Provide a target that pulls in all filesystems.
     systemd.targets.fs =
diff --git a/nixos/modules/tasks/filesystems/bcachefs.nix b/nixos/modules/tasks/filesystems/bcachefs.nix
index ac41ba5f93a..e3ad52a7b05 100644
--- a/nixos/modules/tasks/filesystems/bcachefs.nix
+++ b/nixos/modules/tasks/filesystems/bcachefs.nix
@@ -42,7 +42,11 @@ in
 {
   config = mkIf (elem "bcachefs" config.boot.supportedFilesystems) (mkMerge [
     {
-      system.fsPackages = [ pkgs.bcachefs-tools ];
+      # We do not want to include bachefs in the fsPackages for systemd-initrd
+      # because we provide the unwrapped version of mount.bcachefs
+      # through the extraBin option, which will make it available for use.
+      system.fsPackages = lib.optional (!config.boot.initrd.systemd.enable) pkgs.bcachefs-tools;
+      environment.systemPackages = lib.optional (config.boot.initrd.systemd.enable) pkgs.bcachefs-tools;
 
       # use kernel package with bcachefs support until it's in mainline
       boot.kernelPackages = pkgs.linuxPackages_testing_bcachefs;
@@ -52,7 +56,14 @@ in
       # chacha20 and poly1305 are required only for decryption attempts
       boot.initrd.availableKernelModules = [ "bcachefs" "sha256" "chacha20" "poly1305" ];
 
-      boot.initrd.extraUtilsCommands = ''
+      boot.initrd.systemd.extraBin = {
+        "bcachefs" = "${pkgs.bcachefs-tools}/bin/bcachefs";
+        "mount.bcachefs" = pkgs.runCommand "mount.bcachefs" {} ''
+          cp -pdv ${pkgs.bcachefs-tools}/bin/.mount.bcachefs.sh-wrapped $out
+        '';
+      };
+
+      boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ''
         copy_bin_and_libs ${pkgs.bcachefs-tools}/bin/bcachefs
       '';
       boot.initrd.extraUtilsCommandsTest = ''
diff --git a/nixos/modules/tasks/filesystems/envfs.nix b/nixos/modules/tasks/filesystems/envfs.nix
new file mode 100644
index 00000000000..365cb46ff2f
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/envfs.nix
@@ -0,0 +1,60 @@
+{ pkgs, config, lib, ... }:
+
+let
+  cfg = config.services.envfs;
+  mounts = {
+    "/usr/bin" = {
+      device = "none";
+      fsType = "envfs";
+      options = [
+        "fallback-path=${pkgs.runCommand "fallback-path" {} (''
+          mkdir -p $out
+          ln -s ${config.environment.usrbinenv} $out/env
+          ln -s ${config.environment.binsh} $out/sh
+        '' + cfg.extraFallbackPathCommands)}"
+        "nofail"
+      ];
+    };
+    "/bin" = {
+      device = "/usr/bin";
+      fsType = "none";
+      options = [ "bind" "nofail" ];
+    };
+  };
+in {
+  options = {
+    services.envfs = {
+      enable = lib.mkEnableOption (lib.mdDoc "Envfs filesystem") // {
+        description = lib.mdDoc ''
+          Fuse filesystem that returns symlinks to executables based on the PATH
+          of the requesting process. This is useful to execute shebangs on NixOS
+          that assume hard coded locations in locations like /bin or /usr/bin
+          etc.
+        '';
+      };
+
+      package = lib.mkOption {
+        type = lib.types.package;
+        default = pkgs.envfs;
+        defaultText = lib.literalExpression "pkgs.envfs";
+        description = lib.mdDoc "Which package to use for the envfs.";
+      };
+
+      extraFallbackPathCommands = lib.mkOption {
+        type = lib.types.lines;
+        default = "";
+        example = "ln -s $''{pkgs.bash}/bin/bash $out/bash";
+        description = lib.mdDoc "Extra commands to run in the package that contains fallback executables in case not other executable is found";
+      };
+    };
+  };
+  config = lib.mkIf (cfg.enable) {
+    environment.systemPackages = [ cfg.package ];
+    # we also want these mounts in virtual machines.
+    fileSystems = if config.virtualisation ? qemu then lib.mkVMOverride mounts else mounts;
+
+    # We no longer need those when using envfs
+    system.activationScripts.usrbinenv = lib.mkForce "";
+    system.activationScripts.binsh = lib.mkForce "";
+  };
+}
diff --git a/nixos/modules/tasks/filesystems/vfat.nix b/nixos/modules/tasks/filesystems/vfat.nix
index 5baab1c802c..5421b617b43 100644
--- a/nixos/modules/tasks/filesystems/vfat.nix
+++ b/nixos/modules/tasks/filesystems/vfat.nix
@@ -11,7 +11,7 @@ in
 {
   config = mkIf (any (fs: fs == "vfat") config.boot.supportedFilesystems) {
 
-    system.fsPackages = [ pkgs.dosfstools ];
+    system.fsPackages = [ pkgs.dosfstools pkgs.mtools ];
 
     boot.initrd.kernelModules = mkIf inInitrd [ "vfat" "nls_cp437" "nls_iso8859-1" ];
 
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 4b4f4cc801a..6c775964751 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -97,10 +97,15 @@ let
     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)}";
+  getKeyLocations = pool: if isBool cfgZfs.requestEncryptionCredentials then {
+    hasKeys = cfgZfs.requestEncryptionCredentials;
+    command = "${cfgZfs.package}/sbin/zfs list -rHo name,keylocation,keystatus ${pool}";
+  } else let
+    keys = filter (x: datasetToPool x == pool) cfgZfs.requestEncryptionCredentials;
+  in {
+    hasKeys = keys != [];
+    command = "${cfgZfs.package}/sbin/zfs list -Ho name,keylocation,keystatus ${toString keys}";
+  };
 
   createImportService = { pool, systemd, force, prefix ? "" }:
     nameValuePair "zfs-import-${pool}" {
@@ -124,7 +129,9 @@ let
         RemainAfterExit = true;
       };
       environment.ZFS_FORCE = optionalString force "-f";
-      script = (importLib {
+      script = let
+        keyLocations = getKeyLocations pool;
+      in (importLib {
         # See comments at importLib definition.
         zpoolCmd = "${cfgZfs.package}/sbin/zpool";
         awkCmd = "${pkgs.gawk}/bin/awk";
@@ -139,10 +146,8 @@ let
         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
+          ${optionalString keyLocations.hasKeys ''
+            ${keyLocations.command} | while IFS=$'\t' read ds kl ks; do
               {
               if [[ "$ks" != unavailable ]]; then
                 continue
@@ -503,6 +508,10 @@ in
           assertion = !cfgZfs.forceImportAll || cfgZfs.forceImportRoot;
           message = "If you enable boot.zfs.forceImportAll, you must also enable boot.zfs.forceImportRoot";
         }
+        {
+          assertion = cfgZfs.allowHibernation -> !cfgZfs.forceImportRoot && !cfgZfs.forceImportAll;
+          message = "boot.zfs.allowHibernation while force importing is enabled will cause data corruption";
+        }
       ];
 
       boot = {
@@ -561,7 +570,7 @@ in
               ''
               else concatMapStrings (fs: ''
                 zfs load-key -- ${escapeShellArg fs}
-              '') cfgZfs.requestEncryptionCredentials}
+              '') (filter (x: datasetToPool x == pool) cfgZfs.requestEncryptionCredentials)}
         '') rootPools));
 
         # Systemd in stage 1
diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix
index b24b29c32d4..0fcd3c10219 100644
--- a/nixos/modules/tasks/network-interfaces-systemd.nix
+++ b/nixos/modules/tasks/network-interfaces-systemd.nix
@@ -28,11 +28,164 @@ let
     # TODO: warn the user that any address configured on those interfaces will be useless
     ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches);
 
+  domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain);
+  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;
+        makeGateway = gateway: {
+          routeConfig = {
+            Gateway = gateway;
+            GatewayOnLink = false;
+          };
+        };
+    in optionalAttrs (gateway != [ ]) {
+      routes = override (map makeGateway gateway);
+    } // optionalAttrs (domains != [ ]) {
+      domains = override domains;
+    };
+
+  genericDhcpNetworks = initrd: mkIf cfg.useDHCP {
+    networks."99-ethernet-default-dhcp" = {
+      # 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 (if initrd
+        then config.boot.initrd.systemd.network.wait-online.anyInterface
+        else config.systemd.network.wait-online.anyInterface);
+      networkConfig.IPv6PrivacyExtensions = "kernel";
+    };
+    networks."99-wireless-client-dhcp" = {
+      # 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;
+    };
+  };
+
+
+  interfaceNetworks = mkMerge (forEach interfaces (i: {
+    netdevs = mkIf i.virtual ({
+      "40-${i.name}" = {
+        netdevConfig = {
+          Name = i.name;
+          Kind = i.virtualType;
+        };
+        "${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) {
+          User = i.virtualOwner;
+        };
+      };
+    });
+    networks."40-${i.name}" = mkMerge [ (genericNetwork id) {
+      name = mkDefault i.name;
+      DHCP = mkForce (dhcpStr
+        (if i.useDHCP != null then i.useDHCP else false));
+      address = forEach (interfaceIps i)
+        (ip: "${ip.address}/${toString ip.prefixLength}");
+      routes = forEach (interfaceRoutes i)
+        (route: {
+          # Most of these route options have not been tested.
+          # Please fix or report any mistakes you may find.
+          routeConfig =
+            optionalAttrs (route.address != null && route.prefixLength != null) {
+              Destination = "${route.address}/${toString route.prefixLength}";
+            } //
+            optionalAttrs (route.options ? fastopen_no_cookie) {
+              FastOpenNoCookie = route.options.fastopen_no_cookie;
+            } //
+            optionalAttrs (route.via != null) {
+              Gateway = route.via;
+            } //
+            optionalAttrs (route.type != null) {
+              Type = route.type;
+            } //
+            optionalAttrs (route.options ? onlink) {
+              GatewayOnLink = true;
+            } //
+            optionalAttrs (route.options ? initrwnd) {
+              InitialAdvertisedReceiveWindow = route.options.initrwnd;
+            } //
+            optionalAttrs (route.options ? initcwnd) {
+              InitialCongestionWindow = route.options.initcwnd;
+            } //
+            optionalAttrs (route.options ? pref) {
+              IPv6Preference = route.options.pref;
+            } //
+            optionalAttrs (route.options ? mtu) {
+              MTUBytes = route.options.mtu;
+            } //
+            optionalAttrs (route.options ? metric) {
+              Metric = route.options.metric;
+            } //
+            optionalAttrs (route.options ? src) {
+              PreferredSource = route.options.src;
+            } //
+            optionalAttrs (route.options ? protocol) {
+              Protocol = route.options.protocol;
+            } //
+            optionalAttrs (route.options ? quickack) {
+              QuickAck = route.options.quickack;
+            } //
+            optionalAttrs (route.options ? scope) {
+              Scope = route.options.scope;
+            } //
+            optionalAttrs (route.options ? from) {
+              Source = route.options.from;
+            } //
+            optionalAttrs (route.options ? table) {
+              Table = route.options.table;
+            } //
+            optionalAttrs (route.options ? advmss) {
+              TCPAdvertisedMaximumSegmentSize = route.options.advmss;
+            } //
+            optionalAttrs (route.options ? ttl-propagate) {
+              TTLPropagate = route.options.ttl-propagate == "enabled";
+            };
+        });
+      networkConfig.IPv6PrivacyExtensions = "kernel";
+      linkConfig = optionalAttrs (i.macAddress != null) {
+        MACAddress = i.macAddress;
+      } // optionalAttrs (i.mtu != null) {
+        MTUBytes = toString i.mtu;
+      };
+    }];
+  }));
+
 in
 
 {
+  config = mkMerge [
 
-  config = mkIf cfg.useNetworkd {
+  (mkIf config.boot.initrd.network.enable {
+    # Note this is if initrd.network.enable, not if
+    # initrd.systemd.network.enable. By setting the latter and not the
+    # former, the user retains full control over the configuration.
+    boot.initrd.systemd.network = mkMerge [(genericDhcpNetworks true) interfaceNetworks];
+  })
+
+  (mkIf cfg.useNetworkd {
 
     assertions = [ {
       assertion = cfg.defaultGatewayWindowSize == null;
@@ -54,149 +207,11 @@ in
     networking.dhcpcd.enable = mkDefault false;
 
     systemd.network =
-      let
-        domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain);
-        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;
-              makeGateway = gateway: {
-                routeConfig = {
-                  Gateway = gateway;
-                  GatewayOnLink = false;
-                };
-              };
-          in optionalAttrs (gateway != [ ]) {
-            routes = override (map makeGateway gateway);
-          } // optionalAttrs (domains != [ ]) {
-            domains = override domains;
-          };
-      in mkMerge [ {
+      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}" = {
-            netdevConfig = {
-              Name = i.name;
-              Kind = i.virtualType;
-            };
-            "${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) {
-              User = i.virtualOwner;
-            };
-          };
-        });
-        networks."40-${i.name}" = mkMerge [ (genericNetwork id) {
-          name = mkDefault i.name;
-          DHCP = mkForce (dhcpStr
-            (if i.useDHCP != null then i.useDHCP else false));
-          address = forEach (interfaceIps i)
-            (ip: "${ip.address}/${toString ip.prefixLength}");
-          routes = forEach (interfaceRoutes i)
-            (route: {
-              # Most of these route options have not been tested.
-              # Please fix or report any mistakes you may find.
-              routeConfig =
-                optionalAttrs (route.address != null && route.prefixLength != null) {
-                  Destination = "${route.address}/${toString route.prefixLength}";
-                } //
-                optionalAttrs (route.options ? fastopen_no_cookie) {
-                  FastOpenNoCookie = route.options.fastopen_no_cookie;
-                } //
-                optionalAttrs (route.via != null) {
-                  Gateway = route.via;
-                } //
-                optionalAttrs (route.type != null) {
-                  Type = route.type;
-                } //
-                optionalAttrs (route.options ? onlink) {
-                  GatewayOnLink = true;
-                } //
-                optionalAttrs (route.options ? initrwnd) {
-                  InitialAdvertisedReceiveWindow = route.options.initrwnd;
-                } //
-                optionalAttrs (route.options ? initcwnd) {
-                  InitialCongestionWindow = route.options.initcwnd;
-                } //
-                optionalAttrs (route.options ? pref) {
-                  IPv6Preference = route.options.pref;
-                } //
-                optionalAttrs (route.options ? mtu) {
-                  MTUBytes = route.options.mtu;
-                } //
-                optionalAttrs (route.options ? metric) {
-                  Metric = route.options.metric;
-                } //
-                optionalAttrs (route.options ? src) {
-                  PreferredSource = route.options.src;
-                } //
-                optionalAttrs (route.options ? protocol) {
-                  Protocol = route.options.protocol;
-                } //
-                optionalAttrs (route.options ? quickack) {
-                  QuickAck = route.options.quickack;
-                } //
-                optionalAttrs (route.options ? scope) {
-                  Scope = route.options.scope;
-                } //
-                optionalAttrs (route.options ? from) {
-                  Source = route.options.from;
-                } //
-                optionalAttrs (route.options ? table) {
-                  Table = route.options.table;
-                } //
-                optionalAttrs (route.options ? advmss) {
-                  TCPAdvertisedMaximumSegmentSize = route.options.advmss;
-                } //
-                optionalAttrs (route.options ? ttl-propagate) {
-                  TTLPropagate = route.options.ttl-propagate == "enabled";
-                };
-            });
-          networkConfig.IPv6PrivacyExtensions = "kernel";
-          linkConfig = optionalAttrs (i.macAddress != null) {
-            MACAddress = i.macAddress;
-          } // optionalAttrs (i.mtu != null) {
-            MTUBytes = toString i.mtu;
-          };
-        }];
-      })))
+      (genericDhcpNetworks false)
+      interfaceNetworks
       (mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: {
         netdevs."40-${name}" = {
           netdevConfig = {
@@ -437,6 +452,7 @@ in
               bindsTo = [ "systemd-networkd.service" ];
           };
       };
-  };
+  })
 
+  ];
 }
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 4d47a56ccca..954d7ffba71 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -434,7 +434,8 @@ in
   options = {
 
     networking.hostName = mkOption {
-      default = "nixos";
+      default = config.system.nixos.distroId;
+      defaultText = literalExpression "config.system.nixos.distroId";
       # Only allow hostnames without the domain name part (i.e. no FQDNs, see
       # e.g. "man 5 hostname") and require valid DNS labels (recommended
       # syntax). Note: We also allow underscores for compatibility/legacy
diff --git a/nixos/modules/testing/minimal-kernel.nix b/nixos/modules/testing/minimal-kernel.nix
deleted file mode 100644
index 7c2b9c05cf9..00000000000
--- a/nixos/modules/testing/minimal-kernel.nix
+++ /dev/null
@@ -1,28 +0,0 @@
-{ config, pkgs, lib, ... }:
-
-let
-  configfile = builtins.storePath (builtins.toFile "config" (lib.concatStringsSep "\n"
-    (map (builtins.getAttr "configLine") config.system.requiredKernelConfig))
-  );
-
-  origKernel = pkgs.buildLinux {
-    inherit (pkgs.linux) src version stdenv;
-    inherit configfile;
-    allowImportFromDerivation = true;
-    kernelPatches = [ pkgs.kernelPatches.cifs_timeout_2_6_38 ];
-  };
-
-  kernel = origKernel // (derivation (origKernel.drvAttrs // {
-    configurePhase = ''
-      runHook preConfigure
-      mkdir ../build
-      make $makeFlags "''${makeFlagsArray[@]}" mrproper
-      make $makeFlags "''${makeFlagsArray[@]}" KCONFIG_ALLCONFIG=${configfile} allnoconfig
-      runHook postConfigure
-    '';
-  }));
-
-   kernelPackages = pkgs.linuxPackagesFor kernel;
-in {
-  boot.kernelPackages = kernelPackages;
-}
diff --git a/nixos/modules/testing/test-instrumentation.nix b/nixos/modules/testing/test-instrumentation.nix
index 4ab2578eb81..028099c6464 100644
--- a/nixos/modules/testing/test-instrumentation.nix
+++ b/nixos/modules/testing/test-instrumentation.nix
@@ -96,6 +96,12 @@ in
         MaxLevelConsole=debug
       '';
 
+    boot.initrd.systemd.contents."/etc/systemd/journald.conf".text = ''
+      [Journal]
+      ForwardToConsole=yes
+      MaxLevelConsole=debug
+    '';
+
     systemd.extraConfig = ''
       # Don't clobber the console with duplicate systemd messages.
       ShowStatus=no
@@ -107,6 +113,8 @@ in
       DefaultTimeoutStartSec=300
     '';
 
+    boot.initrd.systemd.extraConfig = config.systemd.extraConfig;
+
     boot.consoleLogLevel = 7;
 
     # Prevent tests from accessing the Internet.
diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix
index 9751f5755f9..aa44f264269 100644
--- a/nixos/modules/virtualisation/amazon-image.nix
+++ b/nixos/modules/virtualisation/amazon-image.nix
@@ -25,11 +25,7 @@ in
 
   config = {
 
-    assertions = [
-      { assertion = versionOlder config.boot.kernelPackages.kernel.version "5.17";
-        message = "ENA driver fails to build with kernel >= 5.17";
-      }
-    ];
+    assertions = [ ];
 
     boot.growPartition = true;
 
@@ -85,7 +81,7 @@ in
     # Allow root logins only using the SSH key that the user specified
     # at instance creation time.
     services.openssh.enable = true;
-    services.openssh.permitRootLogin = "prohibit-password";
+    services.openssh.settings.PermitRootLogin = "prohibit-password";
 
     # Enable the serial console on ttyS0
     systemd.services."serial-getty@ttyS0".enable = true;
diff --git a/nixos/modules/virtualisation/amazon-options.nix b/nixos/modules/virtualisation/amazon-options.nix
index 915bbf9763d..3ea4a6cf781 100644
--- a/nixos/modules/virtualisation/amazon-options.nix
+++ b/nixos/modules/virtualisation/amazon-options.nix
@@ -2,9 +2,6 @@
 let
   inherit (lib) literalExpression types;
 in {
-  imports = [
-    (lib.mkRemovedOptionModule [ "ec2" "hvm" ] "Only HVM instances are supported, so specifying it is no longer necessary.")
-  ];
   options = {
     ec2 = {
       zfs = {
@@ -31,13 +28,13 @@ in {
             options = {
               mount = lib.mkOption {
                 description = lib.mdDoc "Where to mount this dataset.";
-                type = types.nullOr types.string;
+                type = types.nullOr types.str;
                 default = null;
               };
 
               properties = lib.mkOption {
                 description = lib.mdDoc "Properties to set on this dataset.";
-                type = types.attrsOf types.string;
+                type = types.attrsOf types.str;
                 default = {};
               };
             };
@@ -52,6 +49,12 @@ in {
           Whether the EC2 instance is using EFI.
         '';
       };
+      hvm = lib.mkOption {
+        description = "Unused legacy option. While support for non-hvm has been dropped, we keep this option around so that NixOps remains compatible with a somewhat recent `nixpkgs` and machines with an old `stateVersion`.";
+        internal = true;
+        default = true;
+        readOnly = true;
+      };
     };
   };
 
diff --git a/nixos/modules/virtualisation/azure-agent-entropy.patch b/nixos/modules/virtualisation/azure-agent-entropy.patch
deleted file mode 100644
index 2a7ad08a4af..00000000000
--- a/nixos/modules/virtualisation/azure-agent-entropy.patch
+++ /dev/null
@@ -1,17 +0,0 @@
---- a/waagent	2016-03-12 09:58:15.728088851 +0200
-+++ a/waagent	2016-03-12 09:58:43.572680025 +0200
-@@ -6173,10 +6173,10 @@
-             Log("MAC  address: " + ":".join(["%02X" % Ord(a) for a in mac]))
-         
-         # Consume Entropy in ACPI table provided by Hyper-V
--        try:
--            SetFileContents("/dev/random", GetFileContents("/sys/firmware/acpi/tables/OEM0"))
--        except:
--            pass
-+        #try:
-+        #    SetFileContents("/dev/random", GetFileContents("/sys/firmware/acpi/tables/OEM0"))
-+        #except:
-+        #    pass
- 
-         Log("Probing for Azure environment.")
-         self.Endpoint = self.DoDhcpWork()
diff --git a/nixos/modules/virtualisation/azure-agent.nix b/nixos/modules/virtualisation/azure-agent.nix
index abe6455a1a6..6e6021cf80f 100644
--- a/nixos/modules/virtualisation/azure-agent.nix
+++ b/nixos/modules/virtualisation/azure-agent.nix
@@ -1,51 +1,10 @@
 { config, lib, pkgs, ... }:
 
 with lib;
-
 let
 
   cfg = config.virtualisation.azure.agent;
 
-  waagent = with pkgs; stdenv.mkDerivation rec {
-    name = "waagent-2.0";
-    src = pkgs.fetchFromGitHub {
-      owner = "Azure";
-      repo = "WALinuxAgent";
-      rev = "1b3a8407a95344d9d12a2a377f64140975f1e8e4";
-      sha256 = "10byzvmpgrmr4d5mdn2kq04aapqb3sgr1admk13wjmy5cd6bwd2x";
-    };
-
-    patches = [ ./azure-agent-entropy.patch ];
-
-    nativeBuildInputs = [ makeWrapper python pythonPackages.wrapPython ];
-    runtimeDeps = [ findutils gnugrep gawk coreutils openssl openssh
-                    nettools # for hostname
-                    procps # for pidof
-                    shadow # for useradd, usermod
-                    util-linux # for (u)mount, fdisk, sfdisk, mkswap
-                    parted
-                  ];
-    pythonPath = [ pythonPackages.pyasn1 ];
-
-    configurePhase = false;
-    buildPhase = false;
-
-    installPhase = ''
-      substituteInPlace config/99-azure-product-uuid.rules \
-          --replace /bin/chmod "${coreutils}/bin/chmod"
-      mkdir -p $out/lib/udev/rules.d
-      cp config/*.rules $out/lib/udev/rules.d
-
-      mkdir -p $out/bin
-      cp waagent $out/bin/
-      chmod +x $out/bin/waagent
-
-      wrapProgram "$out/bin/waagent" \
-          --prefix PYTHONPATH : $PYTHONPATH \
-          --prefix PATH : "${makeBinPath runtimeDeps}"
-    '';
-  };
-
   provisionedHook = pkgs.writeScript "provisioned-hook" ''
     #!${pkgs.runtimeShell}
     /run/current-system/systemd/bin/systemctl start provisioned.target
@@ -74,14 +33,15 @@ in
 
   ###### implementation
 
-  config = mkIf cfg.enable {
-    assertions = [ {
+  config = lib.mkIf cfg.enable {
+    assertions = [{
       assertion = pkgs.stdenv.hostPlatform.isx86;
       message = "Azure not currently supported on ${pkgs.stdenv.hostPlatform.system}";
-    } {
-      assertion = config.networking.networkmanager.enable == false;
-      message = "Windows Azure Linux Agent is not compatible with NetworkManager";
-    } ];
+    }
+      {
+        assertion = config.networking.networkmanager.enable == false;
+        message = "Windows Azure Linux Agent is not compatible with NetworkManager";
+      }];
 
     boot.initrd.kernelModules = [ "ata_piix" ];
     networking.firewall.allowedUDPPorts = [ 68 ];
@@ -89,13 +49,19 @@ in
 
     environment.etc."waagent.conf".text = ''
         #
-        # Windows Azure Linux Agent Configuration
+        # Microsoft Azure Linux Agent Configuration
         #
 
-        Role.StateConsumer=${provisionedHook}
+        # Enable extension handling. Do not disable this unless you do not need password reset,
+        # backup, monitoring, or any extension handling whatsoever.
+        Extensions.Enabled=y
 
-        # Enable instance creation
-        Provisioning.Enabled=y
+        # How often (in seconds) to poll for new goal states
+        Extensions.GoalStatePeriod=6
+
+        # Which provisioning agent to use. Supported values are "auto" (default), "waagent",
+        # "cloud-init", or "disabled".
+        Provisioning.Agent=disabled
 
         # Password authentication for root account will be unavailable.
         Provisioning.DeleteRootPassword=n
@@ -103,18 +69,31 @@ in
         # Generate fresh host key pair.
         Provisioning.RegenerateSshHostKeyPair=n
 
-        # Supported values are "rsa", "dsa" and "ecdsa".
+        # Supported values are "rsa", "dsa", "ecdsa", "ed25519", and "auto".
+        # The "auto" option is supported on OpenSSH 5.9 (2011) and later.
         Provisioning.SshHostKeyPairType=ed25519
 
         # Monitor host name changes and publish changes via DHCP requests.
         Provisioning.MonitorHostName=y
 
+        # How often (in seconds) to monitor host name changes.
+        Provisioning.MonitorHostNamePeriod=30
+
         # Decode CustomData from Base64.
         Provisioning.DecodeCustomData=n
 
         # Execute CustomData after provisioning.
         Provisioning.ExecuteCustomData=n
 
+        # Algorithm used by crypt when generating password hash.
+        #Provisioning.PasswordCryptId=6
+
+        # Length of random salt used when generating password hash.
+        #Provisioning.PasswordCryptSaltLength=10
+
+        # Allow reset password of sys user
+        Provisioning.AllowResetSysUser=n
+
         # Format if unformatted. If 'n', resource disk will not be mounted.
         ResourceDisk.Format=${if cfg.mountResourceDisk then "y" else "n"}
 
@@ -125,22 +104,103 @@ in
         # Mount point for the resource disk
         ResourceDisk.MountPoint=/mnt/resource
 
-        # Respond to load balancer probes if requested by Windows Azure.
-        LBProbeResponder=y
+        # Create and use swapfile on resource disk.
+        ResourceDisk.EnableSwap=n
+
+        # Size of the swapfile.
+        ResourceDisk.SwapSizeMB=0
 
-        # Enable logging to serial console (y|n)
-        # When stdout is not enough...
-        # 'y' if not set
-        Logs.Console=y
+        # Comma-separated list of mount options. See mount(8) for valid options.
+        ResourceDisk.MountOptions=None
 
         # Enable verbose logging (y|n)
         Logs.Verbose=${if cfg.verboseLogging then "y" else "n"}
 
+        # Enable Console logging, default is y
+        # Logs.Console=y
+
+        # Enable periodic log collection, default is n
+        Logs.Collect=n
+
+        # How frequently to collect logs, default is each hour
+        Logs.CollectPeriod=3600
+
+        # Is FIPS enabled
+        OS.EnableFIPS=n
+
         # Root device timeout in seconds.
         OS.RootDeviceScsiTimeout=300
+
+        # How often (in seconds) to set the root device timeout.
+        OS.RootDeviceScsiTimeoutPeriod=30
+
+        # If "None", the system default version is used.
+        OS.OpensslPath=${pkgs.openssl_3.bin}/bin/openssl
+
+        # Set the SSH ClientAliveInterval
+        # OS.SshClientAliveInterval=180
+
+        # Set the path to SSH keys and configuration files
+        OS.SshDir=/etc/ssh
+
+        # If set, agent will use proxy server to access internet
+        #HttpProxy.Host=None
+        #HttpProxy.Port=None
+
+        # Detect Scvmm environment, default is n
+        # DetectScvmmEnv=n
+
+        #
+        # Lib.Dir=/var/lib/waagent
+
+        #
+        # DVD.MountPoint=/mnt/cdrom/secure
+
+        #
+        # Pid.File=/var/run/waagent.pid
+
+        #
+        # Extension.LogDir=/var/log/azure
+
+        #
+        # Home.Dir=/home
+
+        # Enable RDMA management and set up, should only be used in HPC images
+        OS.EnableRDMA=n
+
+        # Enable checking RDMA driver version and update
+        # OS.CheckRdmaDriver=y
+
+        # Enable or disable goal state processing auto-update, default is enabled
+        AutoUpdate.Enabled=n
+
+        # Determine the update family, this should not be changed
+        # AutoUpdate.GAFamily=Prod
+
+        # Determine if the overprovisioning feature is enabled. If yes, hold extension
+        # handling until inVMArtifactsProfile.OnHold is false.
+        # Default is enabled
+        EnableOverProvisioning=n
+
+        # Allow fallback to HTTP if HTTPS is unavailable
+        # Note: Allowing HTTP (vs. HTTPS) may cause security risks
+        # OS.AllowHTTP=n
+
+        # Add firewall rules to protect access to Azure host node services
+        OS.EnableFirewall=n
+
+        # How often (in seconds) to check the firewall rules
+        OS.EnableFirewallPeriod=30
+
+        # How often (in seconds) to remove the udev rules for persistent network interface
+        # names (75-persistent-net-generator.rules and /etc/udev/rules.d/70-persistent-net.rules)
+        OS.RemovePersistentNetRulesPeriod=30
+
+        # How often (in seconds) to monitor for DHCP client restarts
+        OS.MonitorDhcpClientRestartPeriod=30
     '';
 
-    services.udev.packages = [ waagent ];
+    services.udev.packages = [ pkgs.waagent ];
 
     networking.dhcpcd.persistent = true;
 
@@ -157,23 +217,24 @@ in
       description = "Services Requiring Azure VM provisioning to have finished";
     };
 
-  systemd.services.consume-hypervisor-entropy =
-    { description = "Consume entropy in ACPI table provided by Hyper-V";
-
-      wantedBy = [ "sshd.service" "waagent.service" ];
-      before = [ "sshd.service" "waagent.service" ];
-
-      path  = [ pkgs.coreutils ];
-      script =
-        ''
-          echo "Fetching entropy..."
-          cat /sys/firmware/acpi/tables/OEM0 > /dev/random
-        '';
-      serviceConfig.Type = "oneshot";
-      serviceConfig.RemainAfterExit = true;
-      serviceConfig.StandardError = "journal+console";
-      serviceConfig.StandardOutput = "journal+console";
-     };
+    systemd.services.consume-hypervisor-entropy =
+      {
+        description = "Consume entropy in ACPI table provided by Hyper-V";
+
+        wantedBy = [ "sshd.service" "waagent.service" ];
+        before = [ "sshd.service" "waagent.service" ];
+
+        path = [ pkgs.coreutils ];
+        script =
+          ''
+            echo "Fetching entropy..."
+            cat /sys/firmware/acpi/tables/OEM0 > /dev/random
+          '';
+        serviceConfig.Type = "oneshot";
+        serviceConfig.RemainAfterExit = true;
+        serviceConfig.StandardError = "journal+console";
+        serviceConfig.StandardOutput = "journal+console";
+      };
 
     systemd.services.waagent = {
       wantedBy = [ "multi-user.target" ];
@@ -184,11 +245,10 @@ in
       description = "Windows Azure Agent Service";
       unitConfig.ConditionPathExists = "/etc/waagent.conf";
       serviceConfig = {
-        ExecStart = "${waagent}/bin/waagent -daemon";
+        ExecStart = "${pkgs.waagent}/bin/waagent -daemon";
         Type = "simple";
       };
     };
 
   };
-
 }
diff --git a/nixos/modules/virtualisation/azure-common.nix b/nixos/modules/virtualisation/azure-common.nix
index dc7853b9503..f29d368137a 100644
--- a/nixos/modules/virtualisation/azure-common.nix
+++ b/nixos/modules/virtualisation/azure-common.nix
@@ -30,10 +30,8 @@ with lib;
   # Allow root logins only using the SSH key that the user specified
   # at instance creation time, ping client connections to avoid timeouts
   services.openssh.enable = true;
-  services.openssh.permitRootLogin = "prohibit-password";
-  services.openssh.extraConfig = ''
-    ClientAliveInterval 180
-  '';
+  services.openssh.settings.PermitRootLogin = "prohibit-password";
+  services.openssh.settings.ClientAliveInterval = 180;
 
   # Force getting the hostname from Azure
   networking.hostName = mkDefault "";
diff --git a/nixos/modules/virtualisation/brightbox-image.nix b/nixos/modules/virtualisation/brightbox-image.nix
index 9641b693f18..15f8fd6d8f7 100644
--- a/nixos/modules/virtualisation/brightbox-image.nix
+++ b/nixos/modules/virtualisation/brightbox-image.nix
@@ -103,7 +103,7 @@ in
   # Allow root logins only using the SSH key that the user specified
   # at instance creation time.
   services.openssh.enable = true;
-  services.openssh.permitRootLogin = "prohibit-password";
+  services.openssh.settings.PermitRootLogin = "prohibit-password";
 
   # Force getting the hostname from Google Compute.
   networking.hostName = mkDefault "";
diff --git a/nixos/modules/virtualisation/cloudstack-config.nix b/nixos/modules/virtualisation/cloudstack-config.nix
index 78afebdc5dd..7df3c9c613b 100644
--- a/nixos/modules/virtualisation/cloudstack-config.nix
+++ b/nixos/modules/virtualisation/cloudstack-config.nix
@@ -21,7 +21,7 @@ with lib;
     # Allow root logins
     services.openssh = {
       enable = true;
-      permitRootLogin = "prohibit-password";
+      settings.PermitRootLogin = "prohibit-password";
     };
 
     # Cloud-init configuration.
diff --git a/nixos/modules/virtualisation/containers.nix b/nixos/modules/virtualisation/containers.nix
index fb9c19d79c1..3e33cabf266 100644
--- a/nixos/modules/virtualisation/containers.nix
+++ b/nixos/modules/virtualisation/containers.nix
@@ -1,4 +1,4 @@
-{ config, lib, pkgs, utils, ... }:
+{ config, lib, pkgs, ... }:
 let
   cfg = config.virtualisation.containers;
 
@@ -136,7 +136,7 @@ in
 
     environment.etc."containers/policy.json".source =
       if cfg.policy != { } then pkgs.writeText "policy.json" (builtins.toJSON cfg.policy)
-      else utils.copyFile "${pkgs.skopeo.src}/default-policy.json";
+      else "${pkgs.skopeo.policy}/default-policy.json";
   };
 
 }
diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix
index 95ce1fea58b..dacd700537c 100644
--- a/nixos/modules/virtualisation/cri-o.nix
+++ b/nixos/modules/virtualisation/cri-o.nix
@@ -1,10 +1,13 @@
-{ config, lib, pkgs, utils, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
 let
   cfg = config.virtualisation.cri-o;
 
-  crioPackage = (pkgs.cri-o.override { inherit (cfg) extraPackages; });
+  crioPackage = pkgs.cri-o.override {
+    extraPackages = cfg.extraPackages
+      ++ lib.optional (builtins.elem "zfs" config.boot.supportedFilesystems) config.boot.zfs.package;
+  };
 
   format = pkgs.formats.toml { };
 
@@ -19,7 +22,7 @@ in
     enable = mkEnableOption (lib.mdDoc "Container Runtime Interface for OCI (CRI-O)");
 
     storageDriver = mkOption {
-      type = types.enum [ "btrfs" "overlay" "vfs" ];
+      type = types.enum [ "aufs" "btrfs" "devmapper" "overlay" "vfs" "zfs" ];
       default = "overlay";
       description = lib.mdDoc "Storage driver to be used";
     };
@@ -93,7 +96,7 @@ in
   config = mkIf cfg.enable {
     environment.systemPackages = [ cfg.package pkgs.cri-tools ];
 
-    environment.etc."crictl.yaml".source = utils.copyFile "${pkgs.cri-o-unwrapped.src}/crictl.yaml";
+    environment.etc."crictl.yaml".source = "${cfg.package}/etc/crictl.yaml";
 
     virtualisation.cri-o.settings.crio = {
       storage_driver = cfg.storageDriver;
@@ -124,8 +127,8 @@ in
       };
     };
 
-    environment.etc."cni/net.d/10-crio-bridge.conf".source = utils.copyFile "${pkgs.cri-o-unwrapped.src}/contrib/cni/10-crio-bridge.conf";
-    environment.etc."cni/net.d/99-loopback.conf".source = utils.copyFile "${pkgs.cri-o-unwrapped.src}/contrib/cni/99-loopback.conf";
+    environment.etc."cni/net.d/10-crio-bridge.conflist".source = "${cfg.package}/etc/cni/net.d/10-crio-bridge.conflist";
+    environment.etc."cni/net.d/99-loopback.conflist".source = "${cfg.package}/etc/cni/net.d/99-loopback.conflist";
     environment.etc."crio/crio.conf.d/00-default.conf".source = cfgFile;
 
     # Enable common /etc/containers configuration
diff --git a/nixos/modules/virtualisation/digital-ocean-config.nix b/nixos/modules/virtualisation/digital-ocean-config.nix
index 754bc1a5185..e004b7880aa 100644
--- a/nixos/modules/virtualisation/digital-ocean-config.nix
+++ b/nixos/modules/virtualisation/digital-ocean-config.nix
@@ -49,7 +49,7 @@ with lib;
       };
       services.openssh = {
         enable = mkDefault true;
-        passwordAuthentication = mkDefault false;
+        settings.PasswordAuthentication = mkDefault false;
       };
       services.do-agent.enable = mkDefault true;
       networking = {
diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix
index d9bd10ba1fc..046b8e2f790 100644
--- a/nixos/modules/virtualisation/docker.nix
+++ b/nixos/modules/virtualisation/docker.nix
@@ -100,7 +100,7 @@ in
 
     logDriver =
       mkOption {
-        type = types.enum ["none" "json-file" "syslog" "journald" "gelf" "fluentd" "awslogs" "splunk" "etwlogs" "gcplogs"];
+        type = types.enum ["none" "json-file" "syslog" "journald" "gelf" "fluentd" "awslogs" "splunk" "etwlogs" "gcplogs" "local"];
         default = "journald";
         description =
           lib.mdDoc ''
@@ -163,7 +163,7 @@ in
   ###### implementation
 
   config = mkIf cfg.enable (mkMerge [{
-      boot.kernelModules = [ "bridge" "veth" ];
+      boot.kernelModules = [ "bridge" "veth" "br_netfilter" "xt_nat" ];
       boot.kernel.sysctl = {
         "net.ipv4.conf.all.forwarding" = mkOverride 98 true;
         "net.ipv4.conf.default.forwarding" = mkOverride 98 true;
diff --git a/nixos/modules/virtualisation/ec2-metadata-fetcher.sh b/nixos/modules/virtualisation/ec2-metadata-fetcher.sh
index 9e204d45dbd..716aff7c22f 100644
--- a/nixos/modules/virtualisation/ec2-metadata-fetcher.sh
+++ b/nixos/modules/virtualisation/ec2-metadata-fetcher.sh
@@ -55,10 +55,9 @@ done
 echo "getting EC2 instance metadata..."
 
 get_imds() {
-  # Intentionally no --fail here, so that we proceed even if e.g. a
-  # 404 was returned (but we still fail if we can't reach the IMDS
-  # server).
-  curl --silent --show-error --header "X-aws-ec2-metadata-token: $IMDS_TOKEN" "$@"
+  # --fail to avoid populating missing files with 404 HTML response body
+  # || true to allow the script to continue even when encountering a 404
+  curl --silent --show-error --fail --header "X-aws-ec2-metadata-token: $IMDS_TOKEN" "$@" || true
 }
 
 get_imds -o "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
diff --git a/nixos/modules/virtualisation/google-compute-config.nix b/nixos/modules/virtualisation/google-compute-config.nix
index 44d2a589511..0bed0dc933e 100644
--- a/nixos/modules/virtualisation/google-compute-config.nix
+++ b/nixos/modules/virtualisation/google-compute-config.nix
@@ -29,8 +29,8 @@ with lib;
   # Allow root logins only using SSH keys
   # and disable password authentication in general
   services.openssh.enable = true;
-  services.openssh.permitRootLogin = "prohibit-password";
-  services.openssh.passwordAuthentication = mkDefault false;
+  services.openssh.settings.PermitRootLogin = "prohibit-password";
+  services.openssh.settings.PasswordAuthentication = mkDefault false;
 
   # enable OS Login. This also requires setting enable-oslogin=TRUE metadata on
   # instance or project level
diff --git a/nixos/modules/virtualisation/libvirtd.nix b/nixos/modules/virtualisation/libvirtd.nix
index 3b30bc8c416..7c95405ed31 100644
--- a/nixos/modules/virtualisation/libvirtd.nix
+++ b/nixos/modules/virtualisation/libvirtd.nix
@@ -220,6 +220,17 @@ in
       '';
     };
 
+    parallelShutdown = mkOption {
+      type = types.ints.unsigned;
+      default = 0;
+      description = lib.mdDoc ''
+        Number of guests that will be shutdown concurrently, taking effect when onShutdown
+        is set to "shutdown". If set to 0, guests will be shutdown one after another.
+        Number of guests on shutdown at any time will not exceed number set in this
+        variable.
+      '';
+    };
+
     allowedBridges = mkOption {
       type = types.listOf types.str;
       default = [ "virbr0" ];
@@ -373,6 +384,7 @@ in
 
       environment.ON_BOOT = "${cfg.onBoot}";
       environment.ON_SHUTDOWN = "${cfg.onShutdown}";
+      environment.PARALLEL_SHUTDOWN = "${toString cfg.parallelShutdown}";
     };
 
     systemd.sockets.virtlogd = {
diff --git a/nixos/modules/virtualisation/linode-config.nix b/nixos/modules/virtualisation/linode-config.nix
index d664e8269f4..bbf81bda9c0 100644
--- a/nixos/modules/virtualisation/linode-config.nix
+++ b/nixos/modules/virtualisation/linode-config.nix
@@ -6,8 +6,8 @@ with lib;
   services.openssh = {
     enable = true;
 
-    permitRootLogin = "prohibit-password";
-    passwordAuthentication = mkDefault false;
+    settings.PermitRootLogin = "prohibit-password";
+    settings.PasswordAuthentication = mkDefault false;
   };
 
   networking = {
diff --git a/nixos/modules/virtualisation/linode-image.nix b/nixos/modules/virtualisation/linode-image.nix
index f8d212d9cda..51f793ac011 100644
--- a/nixos/modules/virtualisation/linode-image.nix
+++ b/nixos/modules/virtualisation/linode-image.nix
@@ -62,5 +62,5 @@ in
     };
   };
 
-  meta.maintainers = with maintainers; [ houstdav000 ];
+  meta.maintainers = with maintainers; [ cyntheticfox ];
 }
diff --git a/nixos/modules/virtualisation/lxc-container.nix b/nixos/modules/virtualisation/lxc-container.nix
index 4963d9f3f9e..96b74910224 100644
--- a/nixos/modules/virtualisation/lxc-container.nix
+++ b/nixos/modules/virtualisation/lxc-container.nix
@@ -123,8 +123,8 @@ in
             architecture = builtins.elemAt (builtins.match "^([a-z0-9_]+).+" (toString pkgs.system)) 0;
             creation_date = 1;
             properties = {
-              description = "NixOS ${config.system.nixos.codeName} ${config.system.nixos.label} ${pkgs.system}";
-              os = "nixos";
+              description = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} ${pkgs.system}";
+              os = "${config.system.nixos.distroId}";
               release = "${config.system.nixos.codeName}";
             };
             templates = templates.properties;
@@ -150,6 +150,12 @@ in
           source = config.system.build.toplevel + "/init";
           target = "/sbin/init";
         }
+        # Technically this is not required for lxc, but having also make this configuration work with systemd-nspawn.
+        # Nixos will setup the same symlink after start.
+        {
+          source = config.system.build.toplevel + "/etc/os-release";
+          target = "/etc/os-release";
+        }
       ];
 
       extraCommands = "mkdir -p proc sys dev";
diff --git a/nixos/modules/virtualisation/multipass.nix b/nixos/modules/virtualisation/multipass.nix
new file mode 100644
index 00000000000..b331b3be7ea
--- /dev/null
+++ b/nixos/modules/virtualisation/multipass.nix
@@ -0,0 +1,61 @@
+{ config
+, lib
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.virtualisation.multipass;
+in
+{
+  options = {
+    virtualisation.multipass = {
+      enable = lib.mkEnableOption (lib.mdDoc ''
+        Multipass, a simple manager for virtualised Ubuntu instances.
+      '');
+
+      logLevel = lib.mkOption {
+        type = lib.types.enum [ "error" "warning" "info" "debug" "trace" ];
+        default = "debug";
+        description = lib.mdDoc ''
+          The logging verbosity of the multipassd binary.
+        '';
+      };
+
+      package = lib.mkPackageOptionMD pkgs "multipass" { };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.multipass = {
+      description = "Multipass orchestrates virtual Ubuntu instances.";
+
+      wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
+
+      environment = {
+        "XDG_DATA_HOME" = "/var/lib/multipass/data";
+        "XDG_CACHE_HOME" = "/var/lib/multipass/cache";
+        "XDG_CONFIG_HOME" = "/var/lib/multipass/config";
+      };
+
+      serviceConfig = {
+        ExecStart = "${cfg.package}/bin/multipassd --logger platform --verbosity ${cfg.logLevel}";
+        SyslogIdentifier = "multipassd";
+        Restart = "on-failure";
+        TimeoutStopSec = 300;
+        Type = "simple";
+
+        WorkingDirectory = "/var/lib/multipass";
+
+        StateDirectory = "multipass";
+        StateDirectoryMode = "0750";
+        CacheDirectory = "multipass";
+        CacheDirectoryMode = "0750";
+      };
+    };
+  };
+}
diff --git a/nixos/modules/virtualisation/nixos-containers.nix b/nixos/modules/virtualisation/nixos-containers.nix
index e1e640c4474..d54e2ed3f3a 100644
--- a/nixos/modules/virtualisation/nixos-containers.nix
+++ b/nixos/modules/virtualisation/nixos-containers.nix
@@ -170,11 +170,11 @@ let
         --setenv HOST_PORT="$HOST_PORT" \
         --setenv PATH="$PATH" \
         ${optionalString cfg.ephemeral "--ephemeral"} \
-        ${if cfg.additionalCapabilities != null && cfg.additionalCapabilities != [] then
-          ''--capability="${concatStringsSep "," cfg.additionalCapabilities}"'' else ""
+        ${optionalString (cfg.additionalCapabilities != null && cfg.additionalCapabilities != [])
+          ''--capability="${concatStringsSep "," cfg.additionalCapabilities}"''
         } \
-        ${if cfg.tmpfs != null && cfg.tmpfs != [] then
-          ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}'' else ""
+        ${optionalString (cfg.tmpfs != null && cfg.tmpfs != [])
+          ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}''
         } \
         ${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init"
     '';
@@ -514,6 +514,7 @@ in
                       };
                     in [ extraConfig ] ++ (map (x: x.value) defs);
                   prefix = [ "containers" name ];
+                  inherit (config) specialArgs;
                 }).config;
               };
             };
@@ -555,6 +556,16 @@ in
               '';
             };
 
+            specialArgs = mkOption {
+              type = types.attrsOf types.unspecified;
+              default = {};
+              description = lib.mdDoc ''
+                A set of special arguments to be passed to NixOS modules.
+                This will be merged into the `specialArgs` used to evaluate
+                the NixOS configurations.
+              '';
+            };
+
             ephemeral = mkOption {
               type = types.bool;
               default = false;
diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix
index 61066c3cbd7..a9f4ab77f86 100644
--- a/nixos/modules/virtualisation/oci-containers.nix
+++ b/nixos/modules/virtualisation/oci-containers.nix
@@ -230,7 +230,10 @@ let
     escapedName = escapeShellArg name;
   in {
     wantedBy = [] ++ optional (container.autoStart) "multi-user.target";
-    after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ] ++ dependsOn;
+    after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ]
+            # if imageFile is not set, the service needs the network to download the image from the registry
+            ++ lib.optionals (container.imageFile == null) [ "network-online.target" ]
+            ++ dependsOn;
     requires = dependsOn;
     environment = proxy_env;
 
diff --git a/nixos/modules/virtualisation/openstack-config.nix b/nixos/modules/virtualisation/openstack-config.nix
index af4f5746610..0ef7a3b5010 100644
--- a/nixos/modules/virtualisation/openstack-config.nix
+++ b/nixos/modules/virtualisation/openstack-config.nix
@@ -59,8 +59,8 @@ in
     # Allow root logins
     services.openssh = {
       enable = true;
-      permitRootLogin = "prohibit-password";
-      passwordAuthentication = mkDefault false;
+      settings.PermitRootLogin = "prohibit-password";
+      settings.PasswordAuthentication = mkDefault false;
     };
 
     users.users.root.initialPassword = "foobar";
diff --git a/nixos/modules/virtualisation/openstack-options.nix b/nixos/modules/virtualisation/openstack-options.nix
index c71b581b02c..52f45de92ec 100644
--- a/nixos/modules/virtualisation/openstack-options.nix
+++ b/nixos/modules/virtualisation/openstack-options.nix
@@ -29,13 +29,13 @@ in
             options = {
               mount = lib.mkOption {
                 description = lib.mdDoc "Where to mount this dataset.";
-                type = types.nullOr types.string;
+                type = types.nullOr types.str;
                 default = null;
               };
 
               properties = lib.mkOption {
                 description = lib.mdDoc "Properties to set on this dataset.";
-                type = types.attrsOf types.string;
+                type = types.attrsOf types.str;
                 default = { };
               };
             };
diff --git a/nixos/modules/virtualisation/parallels-guest.nix b/nixos/modules/virtualisation/parallels-guest.nix
index 07a61bf208d..dba8ce02b72 100644
--- a/nixos/modules/virtualisation/parallels-guest.nix
+++ b/nixos/modules/virtualisation/parallels-guest.nix
@@ -87,7 +87,6 @@ in
       bindsTo = [ "cups.service" ];
       path = [ prl-tools ];
       serviceConfig = {
-        Type = "forking";
         ExecStart = "${prl-tools}/bin/prlshprint";
         WorkingDirectory = "${prl-tools}/bin";
       };
diff --git a/nixos/modules/virtualisation/podman/default.nix b/nixos/modules/virtualisation/podman/default.nix
index 118bf82cdd6..c3fae4bac41 100644
--- a/nixos/modules/virtualisation/podman/default.nix
+++ b/nixos/modules/virtualisation/podman/default.nix
@@ -1,13 +1,14 @@
 { config, lib, pkgs, ... }:
 let
   cfg = config.virtualisation.podman;
-  toml = pkgs.formats.toml { };
   json = pkgs.formats.json { };
 
   inherit (lib) mkOption types;
 
   podmanPackage = (pkgs.podman.override {
     extraPackages = cfg.extraPackages
+      # setuid shadow
+      ++ [ "/run/wrappers" ]
       ++ lib.optional (builtins.elem "zfs" config.boot.supportedFilesystems) config.boot.zfs.package;
   });
 
@@ -27,24 +28,13 @@ let
     done
   '';
 
-  net-conflist = pkgs.runCommand "87-podman-bridge.conflist"
-    {
-      nativeBuildInputs = [ pkgs.jq ];
-      extraPlugins = builtins.toJSON cfg.defaultNetwork.extraPlugins;
-      jqScript = ''
-        . + { "plugins": (.plugins + $extraPlugins) }
-      '';
-    } ''
-    jq <${cfg.package}/etc/cni/net.d/87-podman-bridge.conflist \
-      --argjson extraPlugins "$extraPlugins" \
-      "$jqScript" \
-      >$out
-  '';
-
 in
 {
   imports = [
-    ./dnsname.nix
+    (lib.mkRemovedOptionModule [ "virtualisation" "podman" "defaultNetwork" "dnsname" ]
+      "Use virtualisation.podman.defaultNetwork.settings.dns_enabled instead.")
+    (lib.mkRemovedOptionModule [ "virtualisation" "podman" "defaultNetwork" "extraPlugins" ]
+      "Netavark isn't compatible with CNI plugins.")
     ./network-socket.nix
   ];
 
@@ -109,6 +99,37 @@ in
       '';
     };
 
+    autoPrune = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = lib.mdDoc ''
+          Whether to periodically prune Podman resources. If enabled, a
+          systemd timer will run `podman system prune -f`
+          as specified by the `dates` option.
+        '';
+      };
+
+      flags = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "--all" ];
+        description = lib.mdDoc ''
+          Any additional flags passed to {command}`podman system prune`.
+        '';
+      };
+
+      dates = mkOption {
+        default = "weekly";
+        type = types.str;
+        description = lib.mdDoc ''
+          Specification (in the format described by
+          {manpage}`systemd.time(7)`) of the time at
+          which the prune will occur.
+        '';
+      };
+    };
+
     package = lib.mkOption {
       type = types.package;
       default = podmanPackage;
@@ -118,26 +139,42 @@ in
       '';
     };
 
-    defaultNetwork.extraPlugins = lib.mkOption {
-      type = types.listOf json.type;
-      default = [ ];
+    defaultNetwork.settings = lib.mkOption {
+      type = json.type;
+      default = { };
+      example = lib.literalExpression "{ dns_enabled = true; }";
       description = lib.mdDoc ''
-        Extra CNI plugin configurations to add to podman's default network.
+        Settings for podman's default network.
       '';
     };
 
   };
 
-  config = lib.mkIf cfg.enable (lib.mkMerge [
+  config = lib.mkIf cfg.enable
     {
       environment.systemPackages = [ cfg.package ]
         ++ lib.optional cfg.dockerCompat dockerCompat;
 
-      environment.etc."cni/net.d/87-podman-bridge.conflist".source = net-conflist;
+      # https://github.com/containers/podman/blob/097cc6eb6dd8e598c0e8676d21267b4edb11e144/docs/tutorials/basic_networking.md#default-network
+      environment.etc."containers/networks/podman.json" = lib.mkIf (cfg.defaultNetwork.settings != { }) {
+        source = json.generate "podman.json" ({
+          dns_enabled = false;
+          driver = "bridge";
+          id = "0000000000000000000000000000000000000000000000000000000000000000";
+          internal = false;
+          ipam_options = { driver = "host-local"; };
+          ipv6_enabled = false;
+          name = "podman";
+          network_interface = "podman0";
+          subnets = [{ gateway = "10.88.0.1"; subnet = "10.88.0.0/16"; }];
+        } // cfg.defaultNetwork.settings);
+      };
 
       virtualisation.containers = {
         enable = true; # Enable common /etc/containers configuration
-        containersConf.settings = lib.optionalAttrs cfg.enableNvidia {
+        containersConf.settings = {
+          network.network_backend = "netavark";
+        } // lib.optionalAttrs cfg.enableNvidia {
           engine = {
             conmon_env_vars = [ "PATH=${lib.makeBinPath [ pkgs.nvidia-podman ]}" ];
             runtimes.nvidia = [ "${pkgs.nvidia-podman}/bin/nvidia-container-runtime" ];
@@ -147,17 +184,26 @@ in
 
       systemd.packages = [ cfg.package ];
 
-      systemd.services.podman.serviceConfig = {
-        ExecStart = [ "" "${cfg.package}/bin/podman $LOGGING system service" ];
+      systemd.services.podman-prune = {
+        description = "Prune podman resources";
+
+        restartIfChanged = false;
+        unitConfig.X-StopOnRemoval = false;
+
+        serviceConfig.Type = "oneshot";
+
+        script = ''
+          ${cfg.package}/bin/podman system prune -f ${toString cfg.autoPrune.flags}
+        '';
+
+        startAt = lib.optional cfg.autoPrune.enable cfg.autoPrune.dates;
+        after = [ "podman.service" ];
+        requires = [ "podman.service" ];
       };
 
       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 = [
@@ -190,6 +236,5 @@ in
           '';
         }
       ];
-    }
-  ]);
+    };
 }
diff --git a/nixos/modules/virtualisation/podman/dnsname.nix b/nixos/modules/virtualisation/podman/dnsname.nix
deleted file mode 100644
index 3e7d35ae1e4..00000000000
--- a/nixos/modules/virtualisation/podman/dnsname.nix
+++ /dev/null
@@ -1,36 +0,0 @@
-{ config, lib, pkgs, ... }:
-let
-  inherit (lib)
-    mkOption
-    mkIf
-    types
-    ;
-
-  cfg = config.virtualisation.podman;
-
-in
-{
-  options = {
-    virtualisation.podman = {
-
-      defaultNetwork.dnsname.enable = mkOption {
-        type = types.bool;
-        default = false;
-        description = lib.mdDoc ''
-          Enable DNS resolution in the default podman network.
-        '';
-      };
-
-    };
-  };
-
-  config = {
-    virtualisation.containers.containersConf.cniPlugins = mkIf cfg.defaultNetwork.dnsname.enable [ pkgs.dnsname-cni ];
-    virtualisation.podman.defaultNetwork.extraPlugins =
-      lib.optional cfg.defaultNetwork.dnsname.enable {
-        type = "dnsname";
-        domainName = "dns.podman";
-        capabilities.aliases = true;
-      };
-  };
-}
diff --git a/nixos/modules/virtualisation/proxmox-image.nix b/nixos/modules/virtualisation/proxmox-image.nix
index 6a4220fd265..82b33a34179 100644
--- a/nixos/modules/virtualisation/proxmox-image.nix
+++ b/nixos/modules/virtualisation/proxmox-image.nix
@@ -135,10 +135,11 @@ with lib;
     cfgLine = name: value: ''
       ${name}: ${builtins.toString value}
     '';
+    virtio0Storage = builtins.head (builtins.split ":" cfg.qemuConf.virtio0);
     cfgFile = fileName: properties: pkgs.writeTextDir fileName ''
       # generated by NixOS
       ${lib.concatStrings (lib.mapAttrsToList cfgLine properties)}
-      #qmdump#map:virtio0:drive-virtio0:local-lvm:raw:
+      #qmdump#map:virtio0:drive-virtio0:${virtio0Storage}:raw:
     '';
     inherit (cfg) partitionTableType;
     supportEfi = partitionTableType == "efi" || partitionTableType == "hybrid";
@@ -192,6 +193,7 @@ with lib;
             sha256 = "sha256-9rN1x5UfcoQCeYsLqrsthkeMpT1Eztvvq74cRr9G+Dk=";
           };
           patches = [
+            # Proxmox' VMA tool is published as a particular patch upon QEMU
             (pkgs.fetchpatch {
               url =
                 let
@@ -200,6 +202,21 @@ with lib;
                 in "https://git.proxmox.com/?p=pve-qemu.git;a=blob_plain;hb=${rev};f=${path}";
               hash = "sha256-2Dz+ceTwrcyYYxi76RtyY3v15/2pwGcDhFuoZWlgbjc=";
             })
+
+            # Proxmox' VMA tool uses O_DIRECT which fails on tmpfs
+            # Filed to upstream issue tracker: https://bugzilla.proxmox.com/show_bug.cgi?id=4710
+            (pkgs.writeText "inline.patch" ''
+                --- a/vma-writer.c   2023-05-01 15:11:13.361341177 +0200
+                +++ b/vma-writer.c   2023-05-01 15:10:51.785293129 +0200
+                @@ -306,7 +306,7 @@
+                             /* try to use O_NONBLOCK */
+                             fcntl(vmaw->fd, F_SETFL, fcntl(vmaw->fd, F_GETFL)|O_NONBLOCK);
+                         } else  {
+                -            oflags = O_NONBLOCK|O_DIRECT|O_WRONLY|O_EXCL;
+                +            oflags = O_NONBLOCK|O_WRONLY|O_EXCL;
+                             vmaw->fd = qemu_create(filename, oflags, 0644, errp);
+                         }
+            '')
           ];
 
           buildInputs = super.buildInputs ++ [ pkgs.libuuid ];
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index 1b3c0e23f97..5f6bf4b39e9 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -55,6 +55,11 @@ let
 
   };
 
+  selectPartitionTableLayout = { useEFIBoot, useDefaultFilesystems }:
+  if useDefaultFilesystems then
+    if useEFIBoot then "efi" else "legacy"
+  else "none";
+
   driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }:
     let
       drvId = "drive${toString idx}";
@@ -98,7 +103,6 @@ let
   addDeviceNames =
     imap1 (idx: drive: drive // { device = driveDeviceName idx; });
 
-
   # Shell script to start the VM.
   startVM =
     ''
@@ -108,11 +112,26 @@ let
 
       set -e
 
-      NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${config.virtualisation.diskImage}}")
-
-      if ! test -e "$NIX_DISK_IMAGE"; then
-          ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" \
-            ${toString config.virtualisation.diskSize}M
+      NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${toString config.virtualisation.diskImage}}") || test -z "$NIX_DISK_IMAGE"
+
+      if test -n "$NIX_DISK_IMAGE" && ! test -e "$NIX_DISK_IMAGE"; then
+          echo "Disk image do not exist, creating the virtualisation disk image..."
+          # If we are using a bootloader and default filesystems layout.
+          # We have to reuse the system image layout as a backing image format (CoW)
+          # So we can write on the top of it.
+
+          # If we are not using the default FS layout, potentially, we are interested into
+          # performing operations in postDeviceCommands or at early boot on the raw device.
+          # We can still boot through QEMU direct kernel boot feature.
+
+          # CoW prevent size to be attributed to an image.
+          # FIXME: raise this issue to upstream.
+          ${qemu}/bin/qemu-img create \
+          ${concatStringsSep " \\\n" ([ "-f qcow2" ]
+          ++ optional (cfg.useBootLoader && cfg.useDefaultFilesystems) "-F qcow2 -b ${systemImage}/nixos.qcow2"
+          ++ optional (!(cfg.useBootLoader && cfg.useDefaultFilesystems)) "-o size=${toString config.virtualisation.diskSize}M"
+          ++ [ ''"$NIX_DISK_IMAGE"'' ])}
+          echo "Virtualisation disk image created."
       fi
 
       # Create a directory for storing temporary data of the running VM.
@@ -150,22 +169,26 @@ let
       # Create a directory for exchanging data with the VM.
       mkdir -p "$TMPDIR/xchg"
 
-      ${lib.optionalString cfg.useBootLoader
+      ${lib.optionalString cfg.useEFIBoot
       ''
-        # Create a writable copy/snapshot of the boot disk.
-        # A writable boot disk can be booted from automatically.
-        ${qemu}/bin/qemu-img create -f qcow2 -F qcow2 -b ${bootDisk}/disk.img "$TMPDIR/disk.img"
-
-        NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${cfg.efiVars}}")
-
-        ${lib.optionalString cfg.useEFIBoot
-        ''
-          # VM needs writable EFI vars
-          if ! test -e "$NIX_EFI_VARS"; then
-            cp ${bootDisk}/efi-vars.fd "$NIX_EFI_VARS"
-            chmod 0644 "$NIX_EFI_VARS"
-          fi
-        ''}
+        # Expose EFI variables, it's useful even when we are not using a bootloader (!).
+        # We might be interested in having EFI variable storage present even if we aren't booting via UEFI, hence
+        # no guard against `useBootLoader`.  Examples:
+        # - testing PXE boot or other EFI applications
+        # - directbooting LinuxBoot, which `kexec()s` into a UEFI environment that can boot e.g. Windows
+        NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${config.system.name}-efi-vars.fd}")
+        # VM needs writable EFI vars
+        if ! test -e "$NIX_EFI_VARS"; then
+        ${if cfg.useBootLoader then
+            # We still need the EFI var from the make-disk-image derivation
+            # because our "switch-to-configuration" process might
+            # write into it and we want to keep this data.
+            ''cp ${systemImage}/efi-vars.fd "$NIX_EFI_VARS"''
+            else
+            ''cp ${cfg.efi.variables} "$NIX_EFI_VARS"''
+          }
+          chmod 0644 "$NIX_EFI_VARS"
+        fi
       ''}
 
       cd "$TMPDIR"
@@ -198,95 +221,29 @@ let
 
   regInfo = pkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; };
 
-
-  # Generate a hard disk image containing a /boot partition and GRUB
-  # in the MBR.  Used when the `useBootLoader' option is set.
-  # Uses `runInLinuxVM` to create the image in a throwaway VM.
-  # See note [Disk layout with `useBootLoader`].
-  # FIXME: use nixos/lib/make-disk-image.nix.
-  bootDisk =
-    pkgs.vmTools.runInLinuxVM (
-      pkgs.runCommand "nixos-boot-disk"
-        { preVM =
-            ''
-              mkdir $out
-              diskImage=$out/disk.img
-              ${qemu}/bin/qemu-img create -f qcow2 $diskImage "60M"
-              ${if cfg.useEFIBoot then ''
-                efiVars=$out/efi-vars.fd
-                cp ${cfg.efi.variables} $efiVars
-                chmod 0644 $efiVars
-              '' else ""}
-            '';
-          buildInputs = [ pkgs.util-linux ];
-          QEMU_OPTS = "-nographic -serial stdio -monitor none"
-                      + lib.optionalString cfg.useEFIBoot (
-                        " -drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.efi.firmware}"
-                      + " -drive if=pflash,format=raw,unit=1,file=$efiVars");
-        }
-        ''
-          # Create a /boot EFI partition with 60M and arbitrary but fixed GUIDs for reproducibility
-          ${pkgs.gptfdisk}/bin/sgdisk \
-            --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \
-            --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \
-            --attributes=1:set:1 \
-            --attributes=2:set:2 \
-            --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C1 \
-            --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \
-            --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \
-            --hybrid 2 \
-            --recompute-chs /dev/vda
-
-          ${optionalString (config.boot.loader.grub.device != "/dev/vda")
-            # In this throwaway VM, we only have the /dev/vda disk, but the
-            # actual VM described by `config` (used by `switch-to-configuration`
-            # below) may set `boot.loader.grub.device` to a different device
-            # that's nonexistent in the throwaway VM.
-            # Create a symlink for that device, so that the `grub-install`
-            # by `switch-to-configuration` will hit /dev/vda anyway.
-            ''
-              ln -s /dev/vda ${config.boot.loader.grub.device}
-            ''
-          }
-
-          ${pkgs.dosfstools}/bin/mkfs.fat -F16 /dev/vda2
-          export MTOOLS_SKIP_CHECK=1
-          ${pkgs.mtools}/bin/mlabel -i /dev/vda2 ::boot
-
-          # Mount /boot; load necessary modules first.
-          ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_cp437.ko.xz || true
-          ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_iso8859-1.ko.xz || true
-          ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/fat.ko.xz || true
-          ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/vfat.ko.xz || true
-          ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/efivarfs/efivarfs.ko.xz || true
-          mkdir /boot
-          mount /dev/vda2 /boot
-
-          ${optionalString config.boot.loader.efi.canTouchEfiVariables ''
-            mount -t efivarfs efivarfs /sys/firmware/efi/efivars
-          ''}
-
-          # This is needed for GRUB 0.97, which doesn't know about virtio devices.
-          mkdir /boot/grub
-          echo '(hd0) /dev/vda' > /boot/grub/device.map
-
-          # This is needed for systemd-boot to find ESP, and udev is not available here to create this
-          mkdir -p /dev/block
-          ln -s /dev/vda2 /dev/block/254:2
-
-          # Set up system profile (normally done by nixos-rebuild / nix-env --set)
-          mkdir -p /nix/var/nix/profiles
-          ln -s ${config.system.build.toplevel} /nix/var/nix/profiles/system-1-link
-          ln -s /nix/var/nix/profiles/system-1-link /nix/var/nix/profiles/system
-
-          # Install bootloader
-          touch /etc/NIXOS
-          export NIXOS_INSTALL_BOOTLOADER=1
-          ${config.system.build.toplevel}/bin/switch-to-configuration boot
-
-          umount /boot
-        '' # */
-    );
+  # System image is akin to a complete NixOS install with
+  # a boot partition and root partition.
+  systemImage = import ../../lib/make-disk-image.nix {
+    inherit pkgs config lib;
+    additionalPaths = [ regInfo ];
+    format = "qcow2";
+    onlyNixStore = false;
+    partitionTableType = selectPartitionTableLayout { inherit (cfg) useDefaultFilesystems useEFIBoot; };
+    # Bootloader should be installed on the system image only if we are booting through bootloaders.
+    # Though, if a user is not using our default filesystems, it is possible to not have any ESP
+    # or a strange partition table that's incompatible with GRUB configuration.
+    # As a consequence, this may lead to disk image creation failures.
+    # To avoid this, we prefer to let the user find out about how to install the bootloader on its ESP/disk.
+    # Usually, this can be through building your own disk image.
+    # TODO: If a user is interested into a more fine grained heuristic for `installBootLoader`
+    # by examining the actual contents of `cfg.fileSystems`, please send a PR.
+    installBootLoader = cfg.useBootLoader && cfg.useDefaultFilesystems;
+    touchEFIVars = cfg.useEFIBoot;
+    diskSize = "auto";
+    additionalSpace = "0M";
+    copyChannel = false;
+    OVMF = cfg.efi.OVMF;
+  };
 
   storeImage = import ../../lib/make-disk-image.nix {
     inherit pkgs config lib;
@@ -295,17 +252,42 @@ let
     onlyNixStore = true;
     partitionTableType = "none";
     installBootLoader = false;
+    touchEFIVars = false;
     diskSize = "auto";
     additionalSpace = "0M";
     copyChannel = false;
   };
 
+  bootConfiguration =
+    if cfg.useDefaultFilesystems
+    then
+      if cfg.useBootLoader
+      then
+        if cfg.useEFIBoot then "efi_bootloading_with_default_fs"
+        else "legacy_bootloading_with_default_fs"
+      else
+        "direct_boot_with_default_fs"
+    else
+      "custom";
+  suggestedRootDevice = {
+    "efi_bootloading_with_default_fs" = "${cfg.bootLoaderDevice}2";
+    "legacy_bootloading_with_default_fs" = "${cfg.bootLoaderDevice}1";
+    "direct_boot_with_default_fs" = cfg.bootLoaderDevice;
+    # This will enforce a NixOS module type checking error
+    # to ask explicitly the user to set a rootDevice.
+    # As it will look like `rootDevice = lib.mkDefault null;` after
+    # all "computations".
+    "custom" = null;
+  }.${bootConfiguration};
 in
 
 {
   imports = [
     ../profiles/qemu-guest.nix
     (mkRenamedOptionModule [ "virtualisation" "pathsInNixDB" ] [ "virtualisation" "additionalPaths" ])
+    (mkRemovedOptionModule [ "virtualisation" "bootDevice" ] "This option was renamed to `virtualisation.rootDevice`, as it was incorrectly named and misleading. Take the time to review what you want to do and look at the new options like `virtualisation.{bootLoaderDevice, bootPartition}`, open an issue in case of issues.")
+    (mkRemovedOptionModule [ "virtualisation" "efiVars" ] "This option was removed, it is possible to provide a template UEFI variable with `virtualisation.efi.variables` ; if this option is important to you, open an issue")
+    (mkRemovedOptionModule [ "virtualisation" "persistBootDevice" ] "Boot device is always persisted if you use a bootloader through the root disk image ; if this does not work for your usecase, please examine carefully what `virtualisation.{bootDevice, rootDevice, bootPartition}` options offer you and open an issue explaining your need.`")
   ];
 
   options = {
@@ -346,7 +328,7 @@ in
 
     virtualisation.diskImage =
       mkOption {
-        type = types.str;
+        type = types.nullOr types.str;
         default = "./${config.system.name}.qcow2";
         defaultText = literalExpression ''"./''${config.system.name}.qcow2"'';
         description =
@@ -354,16 +336,53 @@ in
             Path to the disk image containing the root filesystem.
             The image will be created on startup if it does not
             exist.
+
+            If null, a tmpfs will be used as the root filesystem and
+            the VM's state will not be persistent.
           '';
       };
 
-    virtualisation.bootDevice =
+    virtualisation.bootLoaderDevice =
       mkOption {
         type = types.path;
+        default = lookupDriveDeviceName "root" cfg.qemu.drives;
+        defaultText = literalExpression ''lookupDriveDeviceName "root" cfg.qemu.drives'';
         example = "/dev/vda";
         description =
           lib.mdDoc ''
-            The disk to be used for the root filesystem.
+            The disk to be used for the boot filesystem.
+            By default, it is the same disk as the root filesystem.
+          '';
+        };
+
+    virtualisation.bootPartition =
+      mkOption {
+        type = types.nullOr types.path;
+        default = if cfg.useEFIBoot then "${cfg.bootLoaderDevice}1" else null;
+        defaultText = literalExpression ''if cfg.useEFIBoot then "''${cfg.bootLoaderDevice}1" else null'';
+        example = "/dev/vda1";
+        description =
+          lib.mdDoc ''
+            The boot partition to be used to mount /boot filesystem.
+            In legacy boots, this should be null.
+            By default, in EFI boot, it is the first partition of the boot device.
+          '';
+      };
+
+    virtualisation.rootDevice =
+      mkOption {
+        type = types.nullOr types.path;
+        example = "/dev/vda2";
+        description =
+          lib.mdDoc ''
+            The disk or partition to be used for the root filesystem.
+            By default (read the source code for more details):
+
+            - under EFI with a bootloader: 2nd partition of the boot disk
+            - in legacy boot with a bootloader: 1st partition of the boot disk
+            - in direct boot (i.e. without a bootloader): whole disk
+
+            In case you are not using a default boot device or a default filesystem, you have to set explicitly your root device.
           '';
       };
 
@@ -528,6 +547,20 @@ in
         '';
     };
 
+    virtualisation.restrictNetwork =
+      mkOption {
+        type = types.bool;
+        default = false;
+        example = true;
+        description =
+          lib.mdDoc ''
+            If this option is enabled, the guest will be isolated, i.e. it will
+            not be able to contact the host and no guest IP packets will be
+            routed over the host to the outside. This option does not affect
+            any explicitly set forwarding rules.
+          '';
+      };
+
     virtualisation.vlans =
       mkOption {
         type = types.listOf types.ints.unsigned;
@@ -549,12 +582,15 @@ in
     virtualisation.writableStore =
       mkOption {
         type = types.bool;
-        default = true; # FIXME
+        default = cfg.mountHostNixStore;
+        defaultText = literalExpression "cfg.mountHostNixStore";
         description =
           lib.mdDoc ''
             If enabled, the Nix store in the VM is made writable by
             layering an overlay filesystem on top of the host's Nix
             store.
+
+            By default, this is enabled if you mount a host Nix store.
           '';
       };
 
@@ -688,6 +724,21 @@ in
           For applications which do a lot of reads from the store,
           this can drastically improve performance, but at the cost of
           disk space and image build time.
+
+          As an alternative, you can use a bootloader which will provide you
+          with a full NixOS system image containing a Nix store and
+          avoid mounting the host nix store through
+          {option}`virtualisation.mountHostNixStore`.
+        '';
+      };
+
+    virtualisation.mountHostNixStore =
+      mkOption {
+        type = types.bool;
+        default = !cfg.useNixStoreImage && !cfg.useBootLoader;
+        defaultText = literalExpression "!cfg.useNixStoreImage && !cfg.useBootLoader";
+        description = lib.mdDoc ''
+          Mount the host Nix store as a 9p mount.
         '';
       };
 
@@ -719,10 +770,22 @@ in
         };
 
     virtualisation.efi = {
+      OVMF = mkOption {
+        type = types.package;
+        default = (pkgs.OVMF.override {
+          secureBoot = cfg.useSecureBoot;
+        }).fd;
+        defaultText = ''(pkgs.OVMF.override {
+          secureBoot = cfg.useSecureBoot;
+        }).fd'';
+        description =
+        lib.mdDoc "OVMF firmware package, defaults to OVMF configured with secure boot if needed.";
+      };
+
       firmware = mkOption {
         type = types.path;
-        default = pkgs.OVMF.firmware;
-        defaultText = literalExpression "pkgs.OVMF.firmware";
+        default = cfg.efi.OVMF.firmware;
+        defaultText = literalExpression "cfg.efi.OVMF.firmware";
         description =
           lib.mdDoc ''
             Firmware binary for EFI implementation, defaults to OVMF.
@@ -731,8 +794,8 @@ in
 
       variables = mkOption {
         type = types.path;
-        default = pkgs.OVMF.variables;
-        defaultText = literalExpression "pkgs.OVMF.variables";
+        default = cfg.efi.OVMF.variables;
+        defaultText = literalExpression "cfg.efi.OVMF.variables";
         description =
           lib.mdDoc ''
             Platform-specific flash binary for EFI variables, implementation-dependent to the EFI firmware.
@@ -756,18 +819,17 @@ in
           '';
       };
 
-    virtualisation.efiVars =
+    virtualisation.useSecureBoot =
       mkOption {
-        type = types.str;
-        default = "./${config.system.name}-efi-vars.fd";
-        defaultText = literalExpression ''"./''${config.system.name}-efi-vars.fd"'';
+        type = types.bool;
+        default = false;
         description =
           lib.mdDoc ''
-            Path to nvram image containing UEFI variables.  The will be created
-            on startup if it does not exist.
+            Enable Secure Boot support in the EFI firmware.
           '';
       };
 
+
     virtualisation.bios =
       mkOption {
         type = types.nullOr types.package;
@@ -823,31 +885,18 @@ in
             ${opt.writableStore} = false;
         '';
 
-    # Note [Disk layout with `useBootLoader`]
-    #
-    # If `useBootLoader = true`, we configure 2 drives:
-    # `/dev/?da` for the root disk, and `/dev/?db` for the boot disk
-    # which has the `/boot` partition and the boot loader.
-    # Concretely:
-    #
-    # * The second drive's image `disk.img` is created in `bootDisk = ...`
-    #   using a throwaway VM. Note that there the disk is always `/dev/vda`,
-    #   even though in the final VM it will be at `/dev/*b`.
-    # * The disks are attached in `virtualisation.qemu.drives`.
-    #   Their order makes them appear as devices `a`, `b`, etc.
-    # * `fileSystems."/boot"` is adjusted to be on device `b`.
-
-    # If `useBootLoader`, GRUB goes to the second disk, see
-    # note [Disk layout with `useBootLoader`].
-    boot.loader.grub.device = mkVMOverride (
-      if cfg.useBootLoader
-        then driveDeviceName 2 # second disk
-        else cfg.bootDevice
-    );
+    # In UEFI boot, we use a EFI-only partition table layout, thus GRUB will fail when trying to install
+    # legacy and UEFI. In order to avoid this, we have to put "nodev" to force UEFI-only installs.
+    # Otherwise, we set the proper bootloader device for this.
+    # FIXME: make a sense of this mess wrt to multiple ESP present in the system, probably use boot.efiSysMountpoint?
+    boot.loader.grub.device = mkVMOverride (if cfg.useEFIBoot then "nodev" else cfg.bootLoaderDevice);
     boot.loader.grub.gfxmodeBios = with cfg.resolution; "${toString x}x${toString y}";
+    virtualisation.rootDevice = mkDefault suggestedRootDevice;
 
     boot.initrd.kernelModules = optionals (cfg.useNixStoreImage && !cfg.writableStore) [ "erofs" ];
 
+    boot.loader.supportsInitrdSecrets = mkIf (!cfg.useBootLoader) (mkVMOverride false);
+
     boot.initrd.extraUtilsCommands = lib.mkIf (cfg.useDefaultFilesystems && !config.boot.initrd.systemd.enable)
       ''
         # We need mke2fs in the initrd.
@@ -858,10 +907,10 @@ in
       ''
         # If the disk image appears to be empty, run mke2fs to
         # initialise.
-        FSTYPE=$(blkid -o value -s TYPE ${cfg.bootDevice} || true)
-        PARTTYPE=$(blkid -o value -s PTTYPE ${cfg.bootDevice} || true)
+        FSTYPE=$(blkid -o value -s TYPE ${cfg.rootDevice} || true)
+        PARTTYPE=$(blkid -o value -s PTTYPE ${cfg.rootDevice} || true)
         if test -z "$FSTYPE" -a -z "$PARTTYPE"; then
-            mke2fs -t ext4 ${cfg.bootDevice}
+            mke2fs -t ext4 ${cfg.rootDevice}
         fi
       '';
 
@@ -878,7 +927,7 @@ in
 
         ${optionalString cfg.writableStore ''
           echo "mounting overlay filesystem on /nix/store..."
-          mkdir -p 0755 $targetRoot/nix/.rw-store/store $targetRoot/nix/.rw-store/work $targetRoot/nix/store
+          mkdir -p -m 0755 $targetRoot/nix/.rw-store/store $targetRoot/nix/.rw-store/work $targetRoot/nix/store
           mount -t overlay overlay $targetRoot/nix/store \
             -o lowerdir=$targetRoot/nix/.ro-store,upperdir=$targetRoot/nix/.rw-store/store,workdir=$targetRoot/nix/.rw-store/work || fail
         ''}
@@ -907,12 +956,10 @@ in
       optional cfg.writableStore "overlay"
       ++ optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx";
 
-    virtualisation.bootDevice = mkDefault (driveDeviceName 1);
-
     virtualisation.additionalPaths = [ config.system.build.toplevel ];
 
     virtualisation.sharedDirectories = {
-      nix-store = mkIf (!cfg.useNixStoreImage) {
+      nix-store = mkIf cfg.mountHostNixStore {
         source = builtins.storeDir;
         target = "/nix/store";
       };
@@ -936,10 +983,11 @@ in
               else "'guestfwd=${proto}:${guest.address}:${toString guest.port}-" +
                    "cmd:${pkgs.netcat}/bin/nc ${host.address} ${toString host.port}',"
           );
+        restrictNetworkOption = lib.optionalString cfg.restrictNetwork "restrict=on,";
       in
       [
         "-net nic,netdev=user.0,model=virtio"
-        "-netdev user,id=user.0,${forwardingOptions}\"$QEMU_NET_OPTS\""
+        "-netdev user,id=user.0,${forwardingOptions}${restrictNetworkOption}\"$QEMU_NET_OPTS\""
       ];
 
     # FIXME: Consolidate this one day.
@@ -964,7 +1012,7 @@ in
       ])
       (mkIf cfg.useEFIBoot [
         "-drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.efi.firmware}"
-        "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS"
+        "-drive if=pflash,format=raw,unit=1,readonly=off,file=$NIX_EFI_VARS"
       ])
       (mkIf (cfg.bios != null) [
         "-bios ${cfg.bios}/bios.bin"
@@ -975,48 +1023,40 @@ in
     ];
 
     virtualisation.qemu.drives = mkMerge [
-      [{
+      (mkIf (cfg.diskImage != null) [{
         name = "root";
         file = ''"$NIX_DISK_IMAGE"'';
         driveExtraOpts.cache = "writeback";
         driveExtraOpts.werror = "report";
-      }]
+        deviceExtraOpts.bootindex = "1";
+      }])
       (mkIf cfg.useNixStoreImage [{
         name = "nix-store";
         file = ''"$TMPDIR"/store.img'';
-        deviceExtraOpts.bootindex = if cfg.useBootLoader then "3" else "2";
+        deviceExtraOpts.bootindex = "2";
         driveExtraOpts.format = if cfg.writableStore then "qcow2" else "raw";
       }])
-      (mkIf cfg.useBootLoader [
-        # The order of this list determines the device names, see
-        # note [Disk layout with `useBootLoader`].
-        {
-          name = "boot";
-          file = ''"$TMPDIR"/disk.img'';
-          driveExtraOpts.media = "disk";
-          deviceExtraOpts.bootindex = "1";
-        }
-      ])
       (imap0 (idx: _: {
         file = "$(pwd)/empty${toString idx}.qcow2";
         driveExtraOpts.werror = "report";
       }) cfg.emptyDiskImages)
     ];
 
+    fileSystems = mkVMOverride cfg.fileSystems;
+
     # Mount the host filesystem via 9P, and bind-mount the Nix store
     # of the host into our own filesystem.  We use mkVMOverride to
     # allow this module to be applied to "normal" NixOS system
     # configuration, where the regular value for the `fileSystems'
     # attribute should be disregarded for the purpose of building a VM
     # test image (since those filesystems don't exist in the VM).
-    fileSystems =
-    let
+    virtualisation.fileSystems = let
       mkSharedDir = tag: share:
         {
           name =
             if tag == "nix-store" && cfg.writableStore
-              then "/nix/.ro-store"
-              else share.target;
+            then "/nix/.ro-store"
+            else share.target;
           value.device = tag;
           value.fsType = "9p";
           value.neededForBoot = true;
@@ -1024,44 +1064,41 @@ in
             [ "trans=virtio" "version=9p2000.L"  "msize=${toString cfg.msize}" ]
             ++ lib.optional (tag == "nix-store") "cache=loose";
         };
-    in
-      mkVMOverride (cfg.fileSystems //
-      optionalAttrs cfg.useDefaultFilesystems {
-        "/".device = cfg.bootDevice;
-        "/".fsType = "ext4";
-        "/".autoFormat = true;
-      } //
-      optionalAttrs config.boot.tmpOnTmpfs {
-        "/tmp" = {
+    in lib.mkMerge [
+      (lib.mapAttrs' mkSharedDir cfg.sharedDirectories)
+      {
+        "/" = lib.mkIf cfg.useDefaultFilesystems (if cfg.diskImage == null then {
+          device = "tmpfs";
+          fsType = "tmpfs";
+        } else {
+          device = cfg.rootDevice;
+          fsType = "ext4";
+          autoFormat = true;
+        });
+        "/tmp" = lib.mkIf config.boot.tmp.useTmpfs {
           device = "tmpfs";
           fsType = "tmpfs";
           neededForBoot = true;
           # Sync with systemd's tmp.mount;
-          options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmpOnTmpfsSize}" ];
+          options = [ "mode=1777" "strictatime" "nosuid" "nodev" "size=${toString config.boot.tmp.tmpfsSize}" ];
         };
-      } //
-      optionalAttrs cfg.useNixStoreImage {
-        "/nix/${if cfg.writableStore then ".ro-store" else "store"}" = {
+        "/nix/${if cfg.writableStore then ".ro-store" else "store"}" = lib.mkIf cfg.useNixStoreImage {
           device = "${lookupDriveDeviceName "nix-store" cfg.qemu.drives}";
           neededForBoot = true;
           options = [ "ro" ];
         };
-      } //
-      optionalAttrs (cfg.writableStore && cfg.writableStoreUseTmpfs) {
-        "/nix/.rw-store" = {
+        "/nix/.rw-store" = lib.mkIf (cfg.writableStore && cfg.writableStoreUseTmpfs) {
           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`
+        "/boot" = lib.mkIf (cfg.useBootLoader && cfg.bootPartition != null) {
+          device = cfg.bootPartition; # 1 for e.g. `vda1`, as created in `systemImage`
           fsType = "vfat";
           noCheck = true; # fsck fails on a r/o filesystem
         };
-      } // lib.mapAttrs' mkSharedDir cfg.sharedDirectories);
+      }
+    ];
 
     boot.initrd.systemd = lib.mkIf (config.boot.initrd.systemd.enable && cfg.writableStore) {
       mounts = [{
@@ -1069,18 +1106,20 @@ in
         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;
+        wantedBy = ["initrd-fs.target"];
+        before = ["initrd-fs.target"];
+        requires = ["rw-store.service"];
+        after = ["rw-store.service"];
+        unitConfig.RequiresMountsFor = "/sysroot/nix/.ro-store";
       }];
       services.rw-store = {
-        after = ["sysroot-nix-.rw\\x2dstore.mount"];
-        unitConfig.DefaultDependencies = false;
+        unitConfig = {
+          DefaultDependencies = false;
+          RequiresMountsFor = "/sysroot/nix/.rw-store";
+        };
         serviceConfig = {
           Type = "oneshot";
-          ExecStart = "/bin/mkdir -p 0755 /sysroot/nix/.rw-store/store /sysroot/nix/.rw-store/work /sysroot/nix/store";
+          ExecStart = "/bin/mkdir -p -m 0755 /sysroot/nix/.rw-store/store /sysroot/nix/.rw-store/work /sysroot/nix/store";
         };
       };
     };
diff --git a/nixos/modules/virtualisation/virtualbox-image.nix b/nixos/modules/virtualisation/virtualbox-image.nix
index 0c095c01ad8..0da217fd1cb 100644
--- a/nixos/modules/virtualisation/virtualbox-image.nix
+++ b/nixos/modules/virtualisation/virtualbox-image.nix
@@ -41,7 +41,7 @@ in {
       };
       vmName = mkOption {
         type = types.str;
-        default = "NixOS ${config.system.nixos.label} (${pkgs.stdenv.hostPlatform.system})";
+        default = "${config.system.nixos.distroName} ${config.system.nixos.label} (${pkgs.stdenv.hostPlatform.system})";
         description = lib.mdDoc ''
           The name of the VirtualBox appliance.
         '';
@@ -81,7 +81,7 @@ in {
       extraDisk = mkOption {
         description = lib.mdDoc ''
           Optional extra disk/hdd configuration.
-          The disk will be an 'ext4' partition on a separate VMDK file.
+          The disk will be an 'ext4' partition on a separate file.
         '';
         default = null;
         example = {
@@ -107,6 +107,46 @@ in {
           };
         });
       };
+      postExportCommands = mkOption {
+        type = types.lines;
+        default = "";
+        example = ''
+          ${pkgs.cot}/bin/cot edit-hardware "$fn" \
+            -v vmx-14 \
+            --nics 2 \
+            --nic-types VMXNET3 \
+            --nic-names 'Nic name' \
+            --nic-networks 'Nic match' \
+            --network-descriptions 'Nic description' \
+            --scsi-subtypes VirtualSCSI
+        '';
+        description = lib.mdDoc ''
+          Extra commands to run after exporting the OVA to `$fn`.
+        '';
+      };
+      storageController = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool (listOf str) ]);
+        example = {
+          name = "SCSI";
+          add = "scsi";
+          portcount = 16;
+          bootable = "on";
+          hostiocache = "on";
+        };
+        default = {
+          name = "SATA";
+          add = "sata";
+          portcount = 4;
+          bootable = "on";
+          hostiocache = "on";
+        };
+        description = lib.mdDoc ''
+          Parameters passed to the VirtualBox appliance. Must have at least
+          `name`.
+
+          Run `VBoxManage storagectl --help` to see more options.
+        '';
+      };
     };
   };
 
@@ -143,8 +183,8 @@ in {
           export HOME=$PWD
           export PATH=${pkgs.virtualbox}/bin:$PATH
 
-          echo "creating VirtualBox pass-through disk wrapper (no copying involved)..."
-          VBoxManage internalcommands createrawvmdk -filename disk.vmdk -rawdisk $diskImage
+          echo "converting image to VirtualBox format..."
+          VBoxManage convertfromraw $diskImage disk.vdi
 
           ${optionalString (cfg.extraDisk != null) ''
             echo "creating extra disk: data-disk.raw"
@@ -156,8 +196,8 @@ in {
               mkpart primary ext4 1MiB -1
             eval $(partx $dataDiskImage -o START,SECTORS --nr 1 --pairs)
             mkfs.ext4 -F -L ${cfg.extraDisk.label} $dataDiskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
-            echo "creating extra disk: data-disk.vmdk"
-            VBoxManage internalcommands createrawvmdk -filename data-disk.vmdk -rawdisk $dataDiskImage
+            echo "creating extra disk: data-disk.vdi"
+            VBoxManage convertfromraw $dataDiskImage data-disk.vdi
           ''}
 
           echo "creating VirtualBox VM..."
@@ -167,18 +207,19 @@ in {
           VBoxManage modifyvm "$vmName" \
             --memory ${toString cfg.memorySize} \
             ${lib.cli.toGNUCommandLineShell { } cfg.params}
-          VBoxManage storagectl "$vmName" --name SATA --add sata --portcount 4 --bootable on --hostiocache on
-          VBoxManage storageattach "$vmName" --storagectl SATA --port 0 --device 0 --type hdd \
-            --medium disk.vmdk
+          VBoxManage storagectl "$vmName" ${lib.cli.toGNUCommandLineShell { } cfg.storageController}
+          VBoxManage storageattach "$vmName" --storagectl ${cfg.storageController.name} --port 0 --device 0 --type hdd \
+            --medium disk.vdi
           ${optionalString (cfg.extraDisk != null) ''
-            VBoxManage storageattach "$vmName" --storagectl SATA --port 1 --device 0 --type hdd \
-            --medium data-disk.vmdk
+            VBoxManage storageattach "$vmName" --storagectl ${cfg.storageController.name} --port 1 --device 0 --type hdd \
+            --medium data-disk.vdi
           ''}
 
           echo "exporting VirtualBox VM..."
           mkdir -p $out
           fn="$out/${cfg.vmFileName}"
           VBoxManage export "$vmName" --output "$fn" --options manifest ${escapeShellArgs cfg.exportParams}
+          ${cfg.postExportCommands}
 
           rm -v $diskImage
 
diff --git a/nixos/modules/virtualisation/waydroid.nix b/nixos/modules/virtualisation/waydroid.nix
index a2cfd806f32..46e5f901015 100644
--- a/nixos/modules/virtualisation/waydroid.nix
+++ b/nixos/modules/virtualisation/waydroid.nix
@@ -56,12 +56,8 @@ in
 
       wantedBy = [ "multi-user.target" ];
 
-      unitConfig = {
-        ConditionPathExists = "/var/lib/waydroid/lxc/waydroid";
-      };
-
       serviceConfig = {
-        ExecStart = "${pkgs.waydroid}/bin/waydroid container start";
+        ExecStart = "${pkgs.waydroid}/bin/waydroid -w container start";
         ExecStop = "${pkgs.waydroid}/bin/waydroid container stop";
         ExecStopPost = "${pkgs.waydroid}/bin/waydroid session stop";
       };
diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix
index 337b5192776..125086294d4 100644
--- a/nixos/release-combined.nix
+++ b/nixos/release-combined.nix
@@ -100,7 +100,6 @@ in rec {
         (onFullSupported "nixos.tests.login")
         (onFullSupported "nixos.tests.misc")
         (onFullSupported "nixos.tests.mutableUsers")
-        (onFullSupported "nixos.tests.nat.firewall-conntrack")
         (onFullSupported "nixos.tests.nat.firewall")
         (onFullSupported "nixos.tests.nat.standalone")
         (onFullSupported "nixos.tests.networking.scripted.bond")
@@ -131,8 +130,7 @@ in rec {
         (onFullSupported "nixos.tests.networking.networkd.virtual")
         (onFullSupported "nixos.tests.networking.networkd.vlan")
         (onFullSupported "nixos.tests.systemd-networkd-ipv6-prefix-delegation")
-        # fails with kernel >= 5.15 https://github.com/NixOS/nixpkgs/pull/152505#issuecomment-1005049314
-        #(onFullSupported "nixos.tests.nfs3.simple")
+        (onFullSupported "nixos.tests.nfs3.simple")
         (onFullSupported "nixos.tests.nfs4.simple")
         (onSystems ["x86_64-linux"] "nixos.tests.oci-containers.podman")
         (onFullSupported "nixos.tests.openssh")
@@ -146,7 +144,8 @@ in rec {
         (onFullSupported "nixos.tests.predictable-interface-names.predictable")
         (onFullSupported "nixos.tests.predictable-interface-names.unpredictableNetworkd")
         (onFullSupported "nixos.tests.predictable-interface-names.unpredictable")
-        (onFullSupported "nixos.tests.printing")
+        (onFullSupported "nixos.tests.printing-service")
+        (onFullSupported "nixos.tests.printing-socket")
         (onFullSupported "nixos.tests.proxy")
         (onFullSupported "nixos.tests.sddm.default")
         (onFullSupported "nixos.tests.shadow")
diff --git a/nixos/release-small.nix b/nixos/release-small.nix
index deb428d1bec..6204dc731ad 100644
--- a/nixos/release-small.nix
+++ b/nixos/release-small.nix
@@ -1,7 +1,11 @@
 # This jobset is used to generate a NixOS channel that contains a
 # small subset of Nixpkgs, mostly useful for servers that need fast
 # security updates.
-
+#
+# Individual jobs can be tested by running:
+#
+#   nix-build nixos/release-small.nix -A <jobname>
+#
 { nixpkgs ? { outPath = (import ../lib).cleanSource ./..; revCount = 56789; shortRev = "gfedcba"; }
 , stableBranch ? false
 , supportedSystems ? [ "aarch64-linux" "x86_64-linux" ] # no i686-linux
@@ -39,8 +43,7 @@ in rec {
         login
         misc
         nat
-        # fails with kernel >= 5.15 https://github.com/NixOS/nixpkgs/pull/152505#issuecomment-1005049314
-        #nfs3
+        nfs3
         openssh
         php
         predictable-interface-names
@@ -82,7 +85,8 @@ in rec {
       stdenv
       subversion
       tarball
-      vim;
+      vim
+      tests-stdenv-gcc-stageCompare;
   };
 
   tested = let
@@ -119,11 +123,9 @@ in rec {
         "nixos.tests.ipv6"
         "nixos.tests.login"
         "nixos.tests.misc"
-        "nixos.tests.nat.firewall-conntrack"
         "nixos.tests.nat.firewall"
         "nixos.tests.nat.standalone"
-        # fails with kernel >= 5.15 https://github.com/NixOS/nixpkgs/pull/152505#issuecomment-1005049314
-        #"nixos.tests.nfs3.simple"
+        "nixos.tests.nfs3.simple"
         "nixos.tests.openssh"
         "nixos.tests.php.fpm"
         "nixos.tests.php.pcre"
@@ -134,6 +136,7 @@ in rec {
         "nixos.tests.proxy"
         "nixos.tests.simple"
         "nixpkgs.jdk"
+        "nixpkgs.tests-stdenv-gcc-stageCompare"
       ])
     ];
   };
diff --git a/nixos/release.nix b/nixos/release.nix
index 919aa86a2d6..93ebe000fc0 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -144,7 +144,6 @@ in rec {
   manual = manualHTML; # TODO(@oxij): remove eventually
   manualEpub = (buildFromConfig ({ ... }: { }) (config: config.system.build.manual.manualEpub));
   manpages = buildFromConfig ({ ... }: { }) (config: config.system.build.manual.manpages);
-  manualGeneratedSources = buildFromConfig ({ ... }: { }) (config: config.system.build.manual.generatedSources);
   options = (buildFromConfig ({ ... }: { }) (config: config.system.build.manual.optionsJSON)).x86_64-linux;
 
 
@@ -181,14 +180,22 @@ in rec {
     inherit system;
   });
 
-  # A variant with a more recent (but possibly less stable) kernel
-  # that might support more hardware.
+  # A variant with a more recent (but possibly less stable) kernel that might support more hardware.
+  # This variant keeps zfs support enabled, hoping it will build and work.
   iso_minimal_new_kernel = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system: makeIso {
     module = ./modules/installer/cd-dvd/installation-cd-minimal-new-kernel.nix;
     type = "minimal-new-kernel";
     inherit system;
   });
 
+  # A variant with a more recent (but possibly less stable) kernel that might support more hardware.
+  # ZFS support disabled since it is unlikely to support the latest kernel.
+  iso_minimal_new_kernel_no_zfs = forMatchingSystems [ "x86_64-linux" "aarch64-linux" ] (system: makeIso {
+    module = ./modules/installer/cd-dvd/installation-cd-minimal-new-kernel-no-zfs.nix;
+    type = "minimal-new-kernel-no-zfs";
+    inherit system;
+  });
+
   sd_image = forMatchingSystems [ "armv6l-linux" "armv7l-linux" "aarch64-linux" ] (system: makeSdImage {
     module = {
         armv6l-linux = ./modules/installer/sd-card/sd-image-raspberrypi-installer.nix;
@@ -206,6 +213,14 @@ in rec {
     inherit system;
   });
 
+  sd_image_new_kernel_no_zfs = forMatchingSystems [ "aarch64-linux" ] (system: makeSdImage {
+    module = {
+        aarch64-linux = ./modules/installer/sd-card/sd-image-aarch64-new-kernel-no-zfs-installer.nix;
+      }.${system};
+    type = "minimal-new-kernel-no-zfs";
+    inherit system;
+  });
+
   # A bootable VirtualBox virtual appliance as an OVA file (i.e. packaged OVF).
   ova = forMatchingSystems [ "x86_64-linux" ] (system:
 
@@ -390,6 +405,12 @@ in rec {
         services.xserver.desktopManager.pantheon.enable = true;
       });
 
+    deepin = makeClosure ({ ... }:
+      { services.xserver.enable = true;
+        services.xserver.displayManager.lightdm.enable = true;
+        services.xserver.desktopManager.deepin.enable = true;
+      });
+
     # Linux/Apache/PostgreSQL/PHP stack.
     lapp = makeClosure ({ pkgs, ... }:
       { services.httpd.enable = true;
diff --git a/nixos/tests/aaaaxy.nix b/nixos/tests/aaaaxy.nix
new file mode 100644
index 00000000000..a1e1d44773c
--- /dev/null
+++ b/nixos/tests/aaaaxy.nix
@@ -0,0 +1,28 @@
+{ pkgs, lib, ... }: {
+  name = "aaaaxy";
+  meta.maintainers = with lib.maintainers; [ Luflosi ];
+
+  nodes.machine = {
+    hardware.opengl.enable = true;
+  };
+
+  # This starts the game from a known state, feeds it a prerecorded set of button presses
+  # and then checks if the final game state is identical to the expected state.
+  # This is also what AAAAXY's CI system does and serves as a good sanity check.
+  testScript = ''
+    machine.wait_for_unit("basic.target")
+
+    machine.succeed(
+      # benchmark.dem needs to be in a mutable directory,
+      # so we can't just refer to the file in the Nix store directly
+      "mkdir -p '/tmp/aaaaxy/assets/demos/'",
+      "ln -s '${pkgs.aaaaxy.testing_infra}/assets/demos/benchmark.dem' '/tmp/aaaaxy/assets/demos/'",
+      """
+        '${pkgs.xvfb-run}/bin/xvfb-run' \
+        '${pkgs.aaaaxy.testing_infra}/scripts/regression-test-demo.sh' \
+        'aaaaxy' 'on track for Any%, All Paths and No Teleports' \
+        '${pkgs.aaaaxy}/bin/aaaaxy' '/tmp/aaaaxy/assets/demos/benchmark.dem'
+      """,
+    )
+  '';
+}
diff --git a/nixos/tests/acme.nix b/nixos/tests/acme.nix
index 64bc99f6d32..d62bf0c0fd9 100644
--- a/nixos/tests/acme.nix
+++ b/nixos/tests/acme.nix
@@ -144,7 +144,11 @@
 
 in {
   name = "acme";
-  meta.maintainers = lib.teams.acme.members;
+  meta = {
+    maintainers = lib.teams.acme.members;
+    # Hard timeout in seconds. Average run time is about 7 minutes.
+    timeout = 1800;
+  };
 
   nodes = {
     # The fake ACME server which will respond to client requests
@@ -357,6 +361,30 @@ in {
       import time
 
 
+      TOTAL_RETRIES = 20
+
+
+      class BackoffTracker(object):
+          delay = 1
+          increment = 1
+
+          def handle_fail(self, retries, message) -> int:
+              assert retries < TOTAL_RETRIES, message
+
+              print(f"Retrying in {self.delay}s, {retries + 1}/{TOTAL_RETRIES}")
+              time.sleep(self.delay)
+
+              # Only increment after the first try
+              if retries == 0:
+                  self.delay += self.increment
+                  self.increment *= 2
+
+              return retries + 1
+
+
+      backoff = BackoffTracker()
+
+
       def switch_to(node, name):
           # On first switch, this will create a symlink to the current system so that we can
           # quickly switch between derivations
@@ -404,9 +432,7 @@ in {
           assert False
 
 
-      def check_connection(node, domain, retries=3):
-          assert retries >= 0, f"Failed to connect to https://{domain}"
-
+      def check_connection(node, domain, retries=0):
           result = node.succeed(
               "openssl s_client -brief -verify 2 -CAfile /tmp/ca.crt"
               f" -servername {domain} -connect {domain}:443 < /dev/null 2>&1"
@@ -414,13 +440,11 @@ in {
 
           for line in result.lower().split("\n"):
               if "verification" in line and "error" in line:
-                  time.sleep(3)
-                  return check_connection(node, domain, retries - 1)
+                  retries = backoff.handle_fail(retries, f"Failed to connect to https://{domain}")
+                  return check_connection(node, domain, retries)
 
 
-      def check_connection_key_bits(node, domain, bits, retries=3):
-          assert retries >= 0, f"Did not find expected number of bits ({bits}) in key"
-
+      def check_connection_key_bits(node, domain, bits, retries=0):
           result = node.succeed(
               "openssl s_client -CAfile /tmp/ca.crt"
               f" -servername {domain} -connect {domain}:443 < /dev/null"
@@ -429,13 +453,11 @@ in {
           print("Key type:", result)
 
           if bits not in result:
-              time.sleep(3)
-              return check_connection_key_bits(node, domain, bits, retries - 1)
-
+              retries = backoff.handle_fail(retries, f"Did not find expected number of bits ({bits}) in key")
+              return check_connection_key_bits(node, domain, bits, retries)
 
-      def check_stapling(node, domain, retries=3):
-          assert retries >= 0, "OCSP Stapling check failed"
 
+      def check_stapling(node, domain, retries=0):
           # Pebble doesn't provide a full OCSP responder, so just check the URL
           result = node.succeed(
               "openssl s_client -CAfile /tmp/ca.crt"
@@ -445,21 +467,19 @@ in {
           print("OCSP Responder URL:", result)
 
           if "${caDomain}:4002" not in result.lower():
-              time.sleep(3)
-              return check_stapling(node, domain, retries - 1)
-
+              retries = backoff.handle_fail(retries, "OCSP Stapling check failed")
+              return check_stapling(node, domain, retries)
 
-      def download_ca_certs(node, retries=5):
-          assert retries >= 0, "Failed to connect to pebble to download root CA certs"
 
+      def download_ca_certs(node, retries=0):
           exit_code, _ = node.execute("curl https://${caDomain}:15000/roots/0 > /tmp/ca.crt")
           exit_code_2, _ = node.execute(
               "curl https://${caDomain}:15000/intermediate-keys/0 >> /tmp/ca.crt"
           )
 
           if exit_code + exit_code_2 > 0:
-              time.sleep(3)
-              return download_ca_certs(node, retries - 1)
+              retries = backoff.handle_fail(retries, "Failed to connect to pebble to download root CA certs")
+              return download_ca_certs(node, retries)
 
 
       start_all()
diff --git a/nixos/tests/aesmd.nix b/nixos/tests/aesmd.nix
index 5da661afd54..848e1c59920 100644
--- a/nixos/tests/aesmd.nix
+++ b/nixos/tests/aesmd.nix
@@ -1,7 +1,7 @@
 { pkgs, lib, ... }: {
   name = "aesmd";
   meta = {
-    maintainers = with lib.maintainers; [ veehaitch ];
+    maintainers = with lib.maintainers; [ trundle veehaitch ];
   };
 
   nodes.machine = { lib, ... }: {
@@ -25,38 +25,78 @@
 
     # We don't have a real SGX machine in NixOS tests
     systemd.services.aesmd.unitConfig.AssertPathExists = lib.mkForce [ ];
+
+    specialisation = {
+      withQuoteProvider.configuration = { ... }: {
+        services.aesmd = {
+          quoteProviderLibrary = pkgs.sgx-azure-dcap-client;
+          environment = {
+            AZDCAP_DEBUG_LOG_LEVEL = "INFO";
+          };
+        };
+      };
+    };
   };
 
-  testScript = ''
-    with subtest("aesmd.service starts"):
-      machine.wait_for_unit("aesmd.service")
-      status, main_pid = machine.systemctl("show --property MainPID --value aesmd.service")
-      assert status == 0, "Could not get MainPID of aesmd.service"
-      main_pid = main_pid.strip()
-
-    with subtest("aesmd.service runtime directory permissions"):
-      runtime_dir = "/run/aesmd";
-      res = machine.succeed(f"stat -c '%a %U %G' {runtime_dir}").strip()
-      assert "750 aesmd sgx" == res, f"{runtime_dir} does not have the expected permissions: {res}"
-
-    with subtest("aesm.socket available on host"):
-      socket_path = "/var/run/aesmd/aesm.socket"
-      machine.wait_until_succeeds(f"test -S {socket_path}")
-      machine.succeed(f"test 777 -eq $(stat -c '%a' {socket_path})")
-      for op in [ "-r", "-w", "-x" ]:
-        machine.succeed(f"sudo -u sgxtest test {op} {socket_path}")
-        machine.fail(f"sudo -u nosgxtest test {op} {socket_path}")
-
-    with subtest("Copies white_list_cert_to_be_verify.bin"):
-      whitelist_path = "/var/opt/aesmd/data/white_list_cert_to_be_verify.bin"
-      whitelist_perms = machine.succeed(
-        f"nsenter -m -t {main_pid} ${pkgs.coreutils}/bin/stat -c '%a' {whitelist_path}"
-      ).strip()
-      assert "644" == whitelist_perms, f"white_list_cert_to_be_verify.bin has permissions {whitelist_perms}"
-
-    with subtest("Writes and binds aesm.conf in service namespace"):
-      aesmd_config = machine.succeed(f"nsenter -m -t {main_pid} ${pkgs.coreutils}/bin/cat /etc/aesmd.conf")
-
-      assert aesmd_config == "whitelist url = http://nixos.org\nproxy type = direct\ndefault quoting type = ecdsa_256\n", "aesmd.conf differs"
-  '';
+  testScript = { nodes, ... }:
+    let
+      specialisations = "${nodes.machine.system.build.toplevel}/specialisation";
+    in
+    ''
+      def get_aesmd_pid():
+        status, main_pid = machine.systemctl("show --property MainPID --value aesmd.service")
+        assert status == 0, "Could not get MainPID of aesmd.service"
+        return main_pid.strip()
+
+      with subtest("aesmd.service starts"):
+        machine.wait_for_unit("aesmd.service")
+
+      main_pid = get_aesmd_pid()
+
+      with subtest("aesmd.service runtime directory permissions"):
+        runtime_dir = "/run/aesmd";
+        res = machine.succeed(f"stat -c '%a %U %G' {runtime_dir}").strip()
+        assert "750 aesmd sgx" == res, f"{runtime_dir} does not have the expected permissions: {res}"
+
+      with subtest("aesm.socket available on host"):
+        socket_path = "/var/run/aesmd/aesm.socket"
+        machine.wait_until_succeeds(f"test -S {socket_path}")
+        machine.succeed(f"test 777 -eq $(stat -c '%a' {socket_path})")
+        for op in [ "-r", "-w", "-x" ]:
+          machine.succeed(f"sudo -u sgxtest test {op} {socket_path}")
+          machine.fail(f"sudo -u nosgxtest test {op} {socket_path}")
+
+      with subtest("Copies white_list_cert_to_be_verify.bin"):
+        whitelist_path = "/var/opt/aesmd/data/white_list_cert_to_be_verify.bin"
+        whitelist_perms = machine.succeed(
+          f"nsenter -m -t {main_pid} ${pkgs.coreutils}/bin/stat -c '%a' {whitelist_path}"
+        ).strip()
+        assert "644" == whitelist_perms, f"white_list_cert_to_be_verify.bin has permissions {whitelist_perms}"
+
+      with subtest("Writes and binds aesm.conf in service namespace"):
+        aesmd_config = machine.succeed(f"nsenter -m -t {main_pid} ${pkgs.coreutils}/bin/cat /etc/aesmd.conf")
+
+        assert aesmd_config == "whitelist url = http://nixos.org\nproxy type = direct\ndefault quoting type = ecdsa_256\n", "aesmd.conf differs"
+
+      with subtest("aesmd.service without quote provider library has correct LD_LIBRARY_PATH"):
+        status, environment = machine.systemctl("show --property Environment --value aesmd.service")
+        assert status == 0, "Could not get Environment of aesmd.service"
+        env_by_name = dict(entry.split("=", 1) for entry in environment.split())
+        assert not env_by_name["LD_LIBRARY_PATH"], "LD_LIBRARY_PATH is not empty"
+
+      with subtest("aesmd.service with quote provider library starts"):
+        machine.succeed('${specialisations}/withQuoteProvider/bin/switch-to-configuration test')
+        machine.wait_for_unit("aesmd.service")
+
+      main_pid = get_aesmd_pid()
+
+      with subtest("aesmd.service with quote provider library has correct LD_LIBRARY_PATH"):
+        ld_library_path = machine.succeed(f"xargs -0 -L1 -a /proc/{main_pid}/environ | grep LD_LIBRARY_PATH")
+        assert ld_library_path.startswith("LD_LIBRARY_PATH=${pkgs.sgx-azure-dcap-client}/lib:"), \
+          "LD_LIBRARY_PATH is not set to the configured quote provider library"
+
+      with subtest("aesmd.service with quote provider library has set AZDCAP_DEBUG_LOG_LEVEL"):
+        azdcp_debug_log_level = machine.succeed(f"xargs -0 -L1 -a /proc/{main_pid}/environ | grep AZDCAP_DEBUG_LOG_LEVEL")
+        assert azdcp_debug_log_level == "AZDCAP_DEBUG_LOG_LEVEL=INFO\n", "AZDCAP_DEBUG_LOG_LEVEL is not set to INFO"
+    '';
 }
diff --git a/nixos/tests/akkoma.nix b/nixos/tests/akkoma.nix
new file mode 100644
index 00000000000..7115c0beed3
--- /dev/null
+++ b/nixos/tests/akkoma.nix
@@ -0,0 +1,121 @@
+/*
+  End-to-end test for Akkoma.
+
+  Based in part on nixos/tests/pleroma.
+
+  TODO: Test federation.
+*/
+import ./make-test-python.nix ({ pkgs, package ? pkgs.akkoma, confined ? false, ... }:
+let
+  userPassword = "4LKOrGo8SgbPm1a6NclVU5Wb";
+
+  provisionUser = pkgs.writers.writeBashBin "provisionUser" ''
+    set -eu -o errtrace -o pipefail
+
+    pleroma_ctl user new jamy jamy@nixos.test --password '${userPassword}' --moderator --admin -y
+  '';
+
+  tlsCert = pkgs.runCommand "selfSignedCerts" {
+    nativeBuildInputs = with pkgs; [ openssl ];
+  } ''
+    mkdir -p $out
+    openssl req -x509 \
+      -subj '/CN=akkoma.nixos.test/' -days 49710 \
+      -addext 'subjectAltName = DNS:akkoma.nixos.test' \
+      -keyout "$out/key.pem" -newkey ed25519 \
+      -out "$out/cert.pem" -noenc
+  '';
+
+  sendToot = pkgs.writers.writeBashBin "sendToot" ''
+    set -eu -o errtrace -o pipefail
+
+    export REQUESTS_CA_BUNDLE="/etc/ssl/certs/ca-certificates.crt"
+
+    echo '${userPassword}' | ${pkgs.toot}/bin/toot login_cli -i "akkoma.nixos.test" -e "jamy@nixos.test"
+    echo "y" | ${pkgs.toot}/bin/toot post "hello world Jamy here"
+    echo "y" | ${pkgs.toot}/bin/toot timeline | grep -F -q "hello world Jamy here"
+
+    # Test file upload
+    echo "y" | ${pkgs.toot}/bin/toot upload <(dd if=/dev/zero bs=1024 count=1024 status=none) \
+      | grep -F -q "https://akkoma.nixos.test/media"
+  '';
+
+  checkFe = pkgs.writers.writeBashBin "checkFe" ''
+    set -eu -o errtrace -o pipefail
+
+    paths=( / /static/{config,styles}.json /pleroma/admin/ )
+
+    for path in "''${paths[@]}"; do
+      diff \
+        <(${pkgs.curl}/bin/curl -f -S -s -o /dev/null -w '%{response_code}' "https://akkoma.nixos.test$path") \
+        <(echo -n 200)
+    done
+  '';
+
+  hosts = nodes: ''
+    ${nodes.akkoma.networking.primaryIPAddress} akkoma.nixos.test
+    ${nodes.client.networking.primaryIPAddress} client.nixos.test
+  '';
+in
+{
+  name = "akkoma";
+  nodes = {
+    client = { nodes, pkgs, config, ... }: {
+      security.pki.certificateFiles = [ "${tlsCert}/cert.pem" ];
+      networking.extraHosts = hosts nodes;
+    };
+
+    akkoma = { nodes, pkgs, config, ... }: {
+      networking.extraHosts = hosts nodes;
+      networking.firewall.allowedTCPPorts = [ 443 ];
+      environment.systemPackages = with pkgs; [ provisionUser ];
+      systemd.services.akkoma.confinement.enable = confined;
+
+      services.akkoma = {
+        enable = true;
+        package = package;
+        config = {
+          ":pleroma" = {
+            ":instance" = {
+              name = "NixOS test Akkoma server";
+              description = "NixOS test Akkoma server";
+              email = "akkoma@nixos.test";
+              notify_email = "akkoma@nixos.test";
+              registration_open = true;
+            };
+
+            ":media_proxy" = {
+              enabled = false;
+            };
+
+            "Pleroma.Web.Endpoint" = {
+              url.host = "akkoma.nixos.test";
+            };
+          };
+        };
+
+        nginx = {
+          addSSL = true;
+          sslCertificate = "${tlsCert}/cert.pem";
+          sslCertificateKey = "${tlsCert}/key.pem";
+        };
+      };
+
+      services.nginx.enable = true;
+      services.postgresql.enable = true;
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    start_all()
+    akkoma.wait_for_unit('akkoma-initdb.service')
+    akkoma.systemctl('restart akkoma-initdb.service')  # test repeated initialisation
+    akkoma.wait_for_unit('akkoma.service')
+    akkoma.wait_for_file('/run/akkoma/socket');
+    akkoma.succeed('${provisionUser}/bin/provisionUser')
+    akkoma.wait_for_unit('nginx.service')
+    client.succeed('${sendToot}/bin/sendToot')
+    client.succeed('${checkFe}/bin/checkFe')
+  '';
+})
+
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 1956d3c9e8c..3c8b163b1fc 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -66,22 +66,36 @@ let
     ;
 
 in {
+
+  # Testing the test driver
+  nixos-test-driver = {
+    extra-python-packages = handleTest ./nixos-test-driver/extra-python-packages.nix {};
+    node-name = runTest ./nixos-test-driver/node-name.nix;
+  };
+
+  # NixOS vm tests and non-vm unit tests
+
   _3proxy = runTest ./3proxy.nix;
+  aaaaxy = runTest ./aaaaxy.nix;
   acme = runTest ./acme.nix;
   adguardhome = runTest ./adguardhome.nix;
-  aesmd = runTest ./aesmd.nix;
+  aesmd = runTestOn ["x86_64-linux"] ./aesmd.nix;
   agate = runTest ./web-servers/agate.nix;
   agda = handleTest ./agda.nix {};
   airsonic = handleTest ./airsonic.nix {};
+  akkoma = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./akkoma.nix {};
+  akkoma-confined = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./akkoma.nix { confined = true; };
   allTerminfo = handleTest ./all-terminfo.nix {};
   alps = handleTest ./alps.nix {};
   amazon-init-shell = handleTest ./amazon-init-shell.nix {};
+  apcupsd = handleTest ./apcupsd.nix {};
   apfs = handleTest ./apfs.nix {};
   apparmor = handleTest ./apparmor.nix {};
   atd = handleTest ./atd.nix {};
   atop = handleTest ./atop.nix {};
   atuin = handleTest ./atuin.nix {};
   auth-mysql = handleTest ./auth-mysql.nix {};
+  authelia = handleTest ./authelia.nix {};
   avahi = handleTest ./avahi.nix {};
   avahi-with-resolved = handleTest ./avahi.nix { networkd = true; };
   babeld = handleTest ./babeld.nix {};
@@ -89,6 +103,7 @@ in {
   bcachefs = handleTestOn ["x86_64-linux" "aarch64-linux"] ./bcachefs.nix {};
   beanstalkd = handleTest ./beanstalkd.nix {};
   bees = handleTest ./bees.nix {};
+  binary-cache = handleTest ./binary-cache.nix {};
   bind = handleTest ./bind.nix {};
   bird = handleTest ./bird.nix {};
   bitcoind = handleTest ./bitcoind.nix {};
@@ -96,6 +111,7 @@ in {
   blockbook-frontend = handleTest ./blockbook-frontend.nix {};
   blocky = handleTest ./blocky.nix {};
   boot = handleTestOn ["x86_64-linux" "aarch64-linux"] ./boot.nix {};
+  bootspec = handleTestOn ["x86_64-linux"] ./bootspec.nix {};
   boot-stage1 = handleTest ./boot-stage1.nix {};
   borgbackup = handleTest ./borgbackup.nix {};
   botamusique = handleTest ./botamusique.nix {};
@@ -103,8 +119,10 @@ in {
   breitbandmessung = handleTest ./breitbandmessung.nix {};
   brscan5 = handleTest ./brscan5.nix {};
   btrbk = handleTest ./btrbk.nix {};
+  btrbk-doas = handleTest ./btrbk-doas.nix {};
   btrbk-no-timer = handleTest ./btrbk-no-timer.nix {};
   btrbk-section-order = handleTest ./btrbk-section-order.nix {};
+  budgie = handleTest ./budgie.nix {};
   buildbot = handleTest ./buildbot.nix {};
   buildkite-agents = handleTest ./buildkite-agents.nix {};
   caddy = handleTest ./caddy.nix {};
@@ -115,22 +133,29 @@ in {
   cassandra_3_0 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_0; };
   cassandra_3_11 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_3_11; };
   cassandra_4 = handleTest ./cassandra.nix { testPackage = pkgs.cassandra_4; };
-  ceph-multi-node = handleTestOn ["x86_64-linux"] ./ceph-multi-node.nix {};
-  ceph-single-node = handleTestOn ["x86_64-linux"] ./ceph-single-node.nix {};
-  ceph-single-node-bluestore = handleTestOn ["x86_64-linux"] ./ceph-single-node-bluestore.nix {};
+  ceph-multi-node = handleTestOn [ "aarch64-linux" "x86_64-linux" ] ./ceph-multi-node.nix {};
+  ceph-single-node = handleTestOn [ "aarch64-linux" "x86_64-linux" ] ./ceph-single-node.nix {};
+  ceph-single-node-bluestore = handleTestOn [ "aarch64-linux" "x86_64-linux" ] ./ceph-single-node-bluestore.nix {};
   certmgr = handleTest ./certmgr.nix {};
   cfssl = handleTestOn ["aarch64-linux" "x86_64-linux"] ./cfssl.nix {};
+  cgit = handleTest ./cgit.nix {};
   charliecloud = handleTest ./charliecloud.nix {};
   chromium = (handleTestOn ["aarch64-linux" "x86_64-linux"] ./chromium.nix {}).stable or {};
+  chrony-ptp = handleTestOn ["aarch64-linux" "x86_64-linux"] ./chrony-ptp.nix {};
   cinnamon = handleTest ./cinnamon.nix {};
   cjdns = handleTest ./cjdns.nix {};
   clickhouse = handleTest ./clickhouse.nix {};
   cloud-init = handleTest ./cloud-init.nix {};
   cloud-init-hostname = handleTest ./cloud-init-hostname.nix {};
+  cloudlog = handleTest ./cloudlog.nix {};
   cntr = handleTestOn ["aarch64-linux" "x86_64-linux"] ./cntr.nix {};
+  cockpit = handleTest ./cockpit.nix {};
   cockroachdb = handleTestOn ["x86_64-linux"] ./cockroachdb.nix {};
+  coder = handleTest ./coder.nix {};
   collectd = handleTest ./collectd.nix {};
+  connman = handleTest ./connman.nix {};
   consul = handleTest ./consul.nix {};
+  consul-template = handleTest ./consul-template.nix {};
   containers-bridge = handleTest ./containers-bridge.nix {};
   containers-custom-pkgs.nix = handleTest ./containers-custom-pkgs.nix {};
   containers-ephemeral = handleTest ./containers-ephemeral.nix {};
@@ -152,8 +177,11 @@ in {
   coturn = handleTest ./coturn.nix {};
   couchdb = handleTest ./couchdb.nix {};
   cri-o = handleTestOn ["aarch64-linux" "x86_64-linux"] ./cri-o.nix {};
+  cups-pdf = handleTest ./cups-pdf.nix {};
   custom-ca = handleTest ./custom-ca.nix {};
   croc = handleTest ./croc.nix {};
+  darling = handleTest ./darling.nix {};
+  deepin = handleTest ./deepin.nix {};
   deluge = handleTest ./deluge.nix {};
   dendrite = handleTest ./matrix/dendrite.nix {};
   dex-oidc = handleTest ./dex-oidc.nix {};
@@ -179,6 +207,7 @@ in {
   dovecot = handleTest ./dovecot.nix {};
   drbd = handleTest ./drbd.nix {};
   earlyoom = handleTestOn ["x86_64-linux"] ./earlyoom.nix {};
+  early-mount-options = handleTest ./early-mount-options.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 {};
@@ -191,55 +220,67 @@ in {
   engelsystem = handleTest ./engelsystem.nix {};
   enlightenment = handleTest ./enlightenment.nix {};
   env = handleTest ./env.nix {};
+  envfs = handleTest ./envfs.nix {};
   envoy = handleTest ./envoy.nix {};
   ergo = handleTest ./ergo.nix {};
   ergochat = handleTest ./ergochat.nix {};
+  esphome = handleTest ./esphome.nix {};
   etc = pkgs.callPackage ../modules/system/etc/test.nix { inherit evalMinimalConfig; };
   activation = pkgs.callPackage ../modules/system/activation/test.nix { };
   etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {};
   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 {};
   evcc = handleTest ./evcc.nix {};
   fancontrol = handleTest ./fancontrol.nix {};
-  fcitx = handleTest ./fcitx {};
+  fcitx5 = handleTest ./fcitx5 {};
   fenics = handleTest ./fenics.nix {};
   ferm = handleTest ./ferm.nix {};
   firefox = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox; };
+  firefox-beta = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-beta; };
+  firefox-devedition = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-devedition; };
   firefox-esr    = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr; }; # used in `tested` job
   firefox-esr-102 = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-esr-102; };
   firejail = handleTest ./firejail.nix {};
-  firewall = handleTest ./firewall.nix {};
+  firewall = handleTest ./firewall.nix { nftables = false; };
+  firewall-nftables = handleTest ./firewall.nix { nftables = true; };
   fish = handleTest ./fish.nix {};
   flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
   fluentd = handleTest ./fluentd.nix {};
   fluidd = handleTest ./fluidd.nix {};
   fontconfig-default-fonts = handleTest ./fontconfig-default-fonts.nix {};
+  forgejo = handleTest ./gitea.nix { giteaPackage = pkgs.forgejo; };
   freenet = handleTest ./freenet.nix {};
   freeswitch = handleTest ./freeswitch.nix {};
-  freshrss = handleTest ./freshrss.nix {};
+  freshrss-sqlite = handleTest ./freshrss-sqlite.nix {};
+  freshrss-pgsql = handleTest ./freshrss-pgsql.nix {};
   frr = handleTest ./frr.nix {};
   fsck = handleTest ./fsck.nix {};
+  fsck-systemd-stage-1 = handleTest ./fsck.nix { systemdStage1 = true; };
   ft2-clone = handleTest ./ft2-clone.nix {};
   mimir = handleTest ./mimir.nix {};
-  garage = handleTest ./garage.nix {};
+  garage = handleTest ./garage {};
+  gemstash = handleTest ./gemstash.nix {};
   gerrit = handleTest ./gerrit.nix {};
   geth = handleTest ./geth.nix {};
   ghostunnel = handleTest ./ghostunnel.nix {};
   gitdaemon = handleTest ./gitdaemon.nix {};
-  gitea = handleTest ./gitea.nix {};
+  gitea = handleTest ./gitea.nix { giteaPackage = pkgs.gitea; };
+  github-runner = handleTest ./github-runner.nix {};
   gitlab = handleTest ./gitlab.nix {};
   gitolite = handleTest ./gitolite.nix {};
   gitolite-fcgiwrap = handleTest ./gitolite-fcgiwrap.nix {};
   glusterfs = handleTest ./glusterfs.nix {};
   gnome = handleTest ./gnome.nix {};
+  gnome-flashback = handleTest ./gnome-flashback.nix {};
   gnome-xorg = handleTest ./gnome-xorg.nix {};
+  gnupg = handleTest ./gnupg.nix {};
   go-neb = handleTest ./go-neb.nix {};
   gobgpd = handleTest ./gobgpd.nix {};
   gocd-agent = handleTest ./gocd-agent.nix {};
   gocd-server = handleTest ./gocd-server.nix {};
   gollum = handleTest ./gollum.nix {};
+  gonic = handleTest ./gonic.nix {};
   google-oslogin = handleTest ./google-oslogin {};
   gotify-server = handleTest ./gotify-server.nix {};
   grafana = handleTest ./grafana {};
@@ -256,6 +297,7 @@ in {
   haste-server = handleTest ./haste-server.nix {};
   haproxy = handleTest ./haproxy.nix {};
   hardened = handleTest ./hardened.nix {};
+  headscale = handleTest ./headscale.nix {};
   healthchecks = handleTest ./web-apps/healthchecks.nix {};
   hbase2 = handleTest ./hbase.nix { package=pkgs.hbase2; };
   hbase_2_4 = handleTest ./hbase.nix { package=pkgs.hbase_2_4; };
@@ -283,13 +325,14 @@ in {
   i3wm = handleTest ./i3wm.nix {};
   icingaweb2 = handleTest ./icingaweb2.nix {};
   iftop = handleTest ./iftop.nix {};
-  ihatemoney = handleTest ./ihatemoney {};
   incron = handleTest ./incron.nix {};
   influxdb = handleTest ./influxdb.nix {};
   initrd-network-openvpn = handleTest ./initrd-network-openvpn {};
   initrd-network-ssh = handleTest ./initrd-network-ssh {};
+  initrd-luks-empty-passphrase = handleTest ./initrd-luks-empty-passphrase.nix {};
   initrdNetwork = handleTest ./initrd-network.nix {};
   initrd-secrets = handleTest ./initrd-secrets.nix {};
+  initrd-secrets-changing = handleTest ./initrd-secrets-changing.nix {};
   input-remapper = handleTest ./input-remapper.nix {};
   inspircd = handleTest ./inspircd.nix {};
   installer = handleTest ./installer.nix {};
@@ -311,6 +354,7 @@ in {
   kafka = handleTest ./kafka.nix {};
   kanidm = handleTest ./kanidm.nix {};
   karma = handleTest ./karma.nix {};
+  kavita = handleTest ./kavita.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 {};
@@ -322,6 +366,7 @@ in {
   keter = handleTest ./keter.nix {};
   kexec = handleTest ./kexec.nix {};
   keycloak = discoverTests (import ./keycloak.nix);
+  keyd = handleTest ./keyd.nix {};
   keymap = handleTest ./keymap.nix {};
   knot = handleTest ./knot.nix {};
   komga = handleTest ./komga.nix {};
@@ -329,7 +374,7 @@ in {
   ksm = handleTest ./ksm.nix {};
   kthxbye = handleTest ./kthxbye.nix {};
   kubernetes = handleTestOn ["x86_64-linux"] ./kubernetes {};
-  kubo = handleTest ./kubo.nix {};
+  kubo = runTest ./kubo.nix;
   ladybird = handleTest ./ladybird.nix {};
   languagetool = handleTest ./languagetool.nix {};
   latestKernel.login = handleTest ./login.nix { latestKernel = true; };
@@ -348,17 +393,19 @@ in {
   limesurvey = handleTest ./limesurvey.nix {};
   listmonk = handleTest ./listmonk.nix {};
   litestream = handleTest ./litestream.nix {};
+  lldap = handleTest ./lldap.nix {};
   locate = handleTest ./locate.nix {};
   login = handleTest ./login.nix {};
   logrotate = handleTest ./logrotate.nix {};
   loki = handleTest ./loki.nix {};
+  luks = handleTest ./luks.nix {};
   lvm2 = handleTest ./lvm2 {};
   lxd = handleTest ./lxd.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 {};
+  maddy = discoverTests (import ./maddy { inherit handleTest; });
   maestral = handleTest ./maestral.nix {};
   magic-wormhole-mailbox-server = handleTest ./magic-wormhole-mailbox-server.nix {};
   magnetico = handleTest ./magnetico.nix {};
@@ -367,6 +414,7 @@ in {
   man = handleTest ./man.nix {};
   mariadb-galera = handleTest ./mysql/mariadb-galera.nix {};
   mastodon = discoverTests (import ./web-apps/mastodon { inherit handleTestOn; });
+  mate = handleTest ./mate.nix {};
   matomo = handleTest ./matomo.nix {};
   matrix-appservice-irc = handleTest ./matrix/appservice-irc.nix {};
   matrix-conduit = handleTest ./matrix/conduit.nix {};
@@ -378,15 +426,18 @@ in {
   memcached = handleTest ./memcached.nix {};
   merecat = handleTest ./merecat.nix {};
   metabase = handleTest ./metabase.nix {};
+  mindustry = handleTest ./mindustry.nix {};
   minecraft = handleTest ./minecraft.nix {};
   minecraft-server = handleTest ./minecraft-server.nix {};
   minidlna = handleTest ./minidlna.nix {};
   miniflux = handleTest ./miniflux.nix {};
   minio = handleTest ./minio.nix {};
+  miriway = handleTest ./miriway.nix {};
   misc = handleTest ./misc.nix {};
   mjolnir = handleTest ./matrix/mjolnir.nix {};
   mod_perl = handleTest ./mod_perl.nix {};
   molly-brown = handleTest ./molly-brown.nix {};
+  monica = handleTest ./web-apps/monica.nix {};
   mongodb = handleTest ./mongodb.nix {};
   moodle = handleTest ./moodle.nix {};
   moonraker = handleTest ./moonraker.nix {};
@@ -396,8 +447,11 @@ in {
   mpd = handleTest ./mpd.nix {};
   mpv = handleTest ./mpv.nix {};
   mtp = handleTest ./mtp.nix {};
+  multipass = handleTest ./multipass.nix {};
   mumble = handleTest ./mumble.nix {};
-  musescore = handleTest ./musescore.nix {};
+  # Fails on aarch64-linux at the PDF creation step - need to debug this on an
+  # aarch64 machine..
+  musescore = handleTestOn ["x86_64-linux"] ./musescore.nix {};
   munin = handleTest ./munin.nix {};
   mutableUsers = handleTest ./mutable-users.nix {};
   mxisd = handleTest ./mxisd.nix {};
@@ -409,8 +463,9 @@ in {
   nagios = handleTest ./nagios.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; };
+  nat.nftables.firewall = handleTest ./nat.nix { withFirewall = true; nftables = true; };
+  nat.nftables.standalone = handleTest ./nat.nix { withFirewall = false; nftables = true; };
   nats = handleTest ./nats.nix {};
   navidrome = handleTest ./navidrome.nix {};
   nbd = handleTest ./nbd.nix {};
@@ -422,8 +477,8 @@ in {
   netdata = handleTest ./netdata.nix {};
   networking.networkd = handleTest ./networking.nix { networkd = true; };
   networking.scripted = handleTest ./networking.nix { networkd = false; };
-  specialisation = handleTest ./specialisation.nix {};
-  netbox = handleTest ./web-apps/netbox.nix {};
+  netbox = handleTest ./web-apps/netbox.nix { inherit (pkgs) netbox; };
+  netbox_3_3 = handleTest ./web-apps/netbox.nix { netbox = pkgs.netbox_3_3; };
   # TODO: put in networking.nix after the test becomes more complete
   networkingProxy = handleTest ./networking-proxy.nix {};
   nextcloud = handleTest ./nextcloud {};
@@ -450,11 +505,13 @@ in {
   nix-serve-ssh = handleTest ./nix-serve-ssh.nix {};
   nixops = handleTest ./nixops/default.nix {};
   nixos-generate-config = handleTest ./nixos-generate-config.nix {};
+  nixos-rebuild-specialisations = handleTest ./nixos-rebuild-specialisations.nix {};
   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 {};
+  noto-fonts-cjk-qt-default-weight = handleTest ./noto-fonts-cjk-qt-default-weight.nix {};
   novacomd = handleTestOn ["x86_64-linux"] ./novacomd.nix {};
   nscd = handleTest ./nscd.nix {};
   nsd = handleTest ./nsd.nix {};
@@ -465,10 +522,12 @@ in {
   ombi = handleTest ./ombi.nix {};
   openarena = handleTest ./openarena.nix {};
   openldap = handleTest ./openldap.nix {};
+  opensearch = discoverTests (import ./opensearch.nix);
   openresty-lua = handleTest ./openresty-lua.nix {};
   opensmtpd = handleTest ./opensmtpd.nix {};
   opensmtpd-rspamd = handleTest ./opensmtpd-rspamd.nix {};
   openssh = handleTest ./openssh.nix {};
+  octoprint = handleTest ./octoprint.nix {};
   openstack-image-metadata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).metadata or {};
   openstack-image-userdata = (handleTestOn ["x86_64-linux"] ./openstack-image.nix {}).userdata or {};
   opentabletdriver = handleTest ./opentabletdriver.nix {};
@@ -494,11 +553,12 @@ in {
   peerflix = handleTest ./peerflix.nix {};
   peering-manager = handleTest ./web-apps/peering-manager.nix {};
   peertube = handleTestOn ["x86_64-linux"] ./web-apps/peertube.nix {};
+  peroxide = handleTest ./peroxide.nix {};
   pgadmin4 = handleTest ./pgadmin4.nix {};
-  pgadmin4-standalone = handleTest ./pgadmin4-standalone.nix {};
   pgjwt = handleTest ./pgjwt.nix {};
   pgmanage = handleTest ./pgmanage.nix {};
   phosh = handleTest ./phosh.nix {};
+  photoprism = handleTest ./photoprism.nix {};
   php = handleTest ./php {};
   php80 = handleTest ./php { php = pkgs.php80; };
   php81 = handleTest ./php { php = pkgs.php81; };
@@ -516,7 +576,6 @@ in {
   plotinus = handleTest ./plotinus.nix {};
   podgrab = handleTest ./podgrab.nix {};
   podman = handleTestOn ["aarch64-linux" "x86_64-linux"] ./podman/default.nix {};
-  podman-dnsname = handleTestOn ["aarch64-linux" "x86_64-linux"] ./podman/dnsname.nix {};
   podman-tls-ghostunnel = handleTestOn ["aarch64-linux" "x86_64-linux"] ./podman/tls-ghostunnel.nix {};
   polaris = handleTest ./polaris.nix {};
   pomerium = handleTestOn ["x86_64-linux"] ./pomerium.nix {};
@@ -525,13 +584,15 @@ in {
   postfixadmin = handleTest ./postfixadmin.nix {};
   postgis = handleTest ./postgis.nix {};
   postgresql = handleTest ./postgresql.nix {};
+  postgresql-jit = handleTest ./postgresql-jit.nix {};
   postgresql-wal-receiver = handleTest ./postgresql-wal-receiver.nix {};
   powerdns = handleTest ./powerdns.nix {};
   powerdns-admin = handleTest ./powerdns-admin.nix {};
   power-profiles-daemon = handleTest ./power-profiles-daemon.nix {};
   pppd = handleTest ./pppd.nix {};
   predictable-interface-names = handleTest ./predictable-interface-names.nix {};
-  printing = handleTest ./printing.nix {};
+  printing-socket = handleTest ./printing.nix { socket = true; };
+  printing-service = handleTest ./printing.nix { socket = false; };
   privacyidea = handleTest ./privacyidea.nix {};
   privoxy = handleTest ./privoxy.nix {};
   prometheus = handleTest ./prometheus.nix {};
@@ -543,14 +604,17 @@ in {
   pt2-clone = handleTest ./pt2-clone.nix {};
   pykms = handleTest ./pykms.nix {};
   public-inbox = handleTest ./public-inbox.nix {};
+  pufferpanel = handleTest ./pufferpanel.nix {};
   pulseaudio = discoverTests (import ./pulseaudio.nix);
   qboot = handleTestOn ["x86_64-linux" "i686-linux"] ./qboot.nix {};
+  qemu-vm-restrictnetwork = handleTest ./qemu-vm-restrictnetwork.nix {};
   quorum = handleTest ./quorum.nix {};
   quake3 = handleTest ./quake3.nix {};
   rabbitmq = handleTest ./rabbitmq.nix {};
   radarr = handleTest ./radarr.nix {};
   radicale = handleTest ./radicale.nix {};
   rasdaemon = handleTest ./rasdaemon.nix {};
+  readarr = handleTest ./readarr.nix {};
   redis = handleTest ./redis.nix {};
   redmine = handleTest ./redmine.nix {};
   restartByActivationScript = handleTest ./restart-by-activation-script.nix {};
@@ -574,6 +638,7 @@ in {
   searx = handleTest ./searx.nix {};
   service-runner = handleTest ./service-runner.nix {};
   sfxr-qt = handleTest ./sfxr-qt.nix {};
+  sgtpuzzles = handleTest ./sgtpuzzles.nix {};
   shadow = handleTest ./shadow.nix {};
   shadowsocks = handleTest ./shadowsocks {};
   shattered-pixel-dungeon = handleTest ./shattered-pixel-dungeon.nix {};
@@ -584,10 +649,10 @@ in {
   smokeping = handleTest ./smokeping.nix {};
   snapcast = handleTest ./snapcast.nix {};
   snapper = handleTest ./snapper.nix {};
+  snipe-it = runTest ./web-apps/snipe-it.nix;
   soapui = handleTest ./soapui.nix {};
   sogo = handleTest ./sogo.nix {};
   solanum = handleTest ./solanum.nix {};
-  solr = handleTest ./solr.nix {};
   sonarr = handleTest ./sonarr.nix {};
   sourcehut = handleTest ./sourcehut.nix {};
   spacecookie = handleTest ./spacecookie.nix {};
@@ -596,12 +661,14 @@ in {
   sslh = handleTest ./sslh.nix {};
   sssd = handleTestOn ["x86_64-linux"] ./sssd.nix {};
   sssd-ldap = handleTestOn ["x86_64-linux"] ./sssd-ldap.nix {};
+  stargazer = runTest ./web-servers/stargazer.nix;
   starship = handleTest ./starship.nix {};
   step-ca = handleTestOn ["x86_64-linux"] ./step-ca.nix {};
   stratis = handleTest ./stratis {};
   strongswan-swanctl = handleTest ./strongswan-swanctl.nix {};
   stunnel = handleTest ./stunnel.nix {};
   sudo = handleTest ./sudo.nix {};
+  swap-file-btrfs = handleTest ./swap-file-btrfs.nix {};
   swap-partition = handleTest ./swap-partition.nix {};
   sway = handleTest ./sway.nix {};
   switchTest = handleTest ./switch-test.nix {};
@@ -617,16 +684,22 @@ in {
   systemd-confinement = handleTest ./systemd-confinement.nix {};
   systemd-coredump = handleTest ./systemd-coredump.nix {};
   systemd-cryptenroll = handleTest ./systemd-cryptenroll.nix {};
+  systemd-credentials-tpm2 = handleTest ./systemd-credentials-tpm2.nix {};
   systemd-escaping = handleTest ./systemd-escaping.nix {};
   systemd-initrd-btrfs-raid = handleTest ./systemd-initrd-btrfs-raid.nix {};
   systemd-initrd-luks-fido2 = handleTest ./systemd-initrd-luks-fido2.nix {};
   systemd-initrd-luks-keyfile = handleTest ./systemd-initrd-luks-keyfile.nix {};
+  systemd-initrd-luks-empty-passphrase = handleTest ./initrd-luks-empty-passphrase.nix { systemdStage1 = true; };
   systemd-initrd-luks-password = handleTest ./systemd-initrd-luks-password.nix {};
   systemd-initrd-luks-tpm2 = handleTest ./systemd-initrd-luks-tpm2.nix {};
   systemd-initrd-modprobe = handleTest ./systemd-initrd-modprobe.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-initrd-vconsole = handleTest ./systemd-initrd-vconsole.nix {};
+  systemd-initrd-networkd = handleTest ./systemd-initrd-networkd.nix {};
+  systemd-initrd-networkd-ssh = handleTest ./systemd-initrd-networkd-ssh.nix {};
+  systemd-initrd-networkd-openvpn = handleTest ./initrd-network-openvpn { systemdStage1 = true; };
   systemd-journal = handleTest ./systemd-journal.nix {};
   systemd-machinectl = handleTest ./systemd-machinectl.nix {};
   systemd-networkd = handleTest ./systemd-networkd.nix {};
@@ -638,9 +711,13 @@ in {
   systemd-nspawn = handleTest ./systemd-nspawn.nix {};
   systemd-oomd = handleTest ./systemd-oomd.nix {};
   systemd-portabled = handleTest ./systemd-portabled.nix {};
+  systemd-repart = handleTest ./systemd-repart.nix {};
   systemd-shutdown = handleTest ./systemd-shutdown.nix {};
   systemd-timesyncd = handleTest ./systemd-timesyncd.nix {};
+  systemd-user-tmpfiles-rules = handleTest ./systemd-user-tmpfiles-rules.nix {};
   systemd-misc = handleTest ./systemd-misc.nix {};
+  systemd-userdbd = handleTest ./systemd-userdbd.nix {};
+  systemd-homed = handleTest ./systemd-homed.nix {};
   tandoor-recipes = handleTest ./tandoor-recipes.nix {};
   taskserver = handleTest ./taskserver.nix {};
   tayga = handleTest ./tayga.nix {};
@@ -651,6 +728,8 @@ in {
   terminal-emulators = handleTest ./terminal-emulators.nix {};
   tiddlywiki = handleTest ./tiddlywiki.nix {};
   tigervnc = handleTest ./tigervnc.nix {};
+  timescaledb = handleTest ./timescaledb.nix {};
+  promscale = handleTest ./promscale.nix {};
   timezone = handleTest ./timezone.nix {};
   tinc = handleTest ./tinc {};
   tinydns = handleTest ./tinydns.nix {};
@@ -673,6 +752,7 @@ in {
   tuxguitar = handleTest ./tuxguitar.nix {};
   ucarp = handleTest ./ucarp.nix {};
   udisks2 = handleTest ./udisks2.nix {};
+  ulogd = handleTest ./ulogd.nix {};
   unbound = handleTest ./unbound.nix {};
   unifi = handleTest ./unifi.nix {};
   unit-php = handleTest ./web-servers/unit-php.nix {};
@@ -687,6 +767,7 @@ in {
   varnish60 = handleTest ./varnish.nix { package = pkgs.varnish60; };
   varnish72 = handleTest ./varnish.nix { package = pkgs.varnish72; };
   vault = handleTest ./vault.nix {};
+  vault-agent = handleTest ./vault-agent.nix {};
   vault-dev = handleTest ./vault-dev.nix {};
   vault-postgresql = handleTest ./vault-postgresql.nix {};
   vaultwarden = handleTest ./vaultwarden.nix {};
@@ -699,6 +780,7 @@ in {
   vsftpd = handleTest ./vsftpd.nix {};
   warzone2100 = handleTest ./warzone2100.nix {};
   wasabibackend = handleTest ./wasabibackend.nix {};
+  webhook = runTest ./webhook.nix;
   wiki-js = handleTest ./wiki-js.nix {};
   wine = handleTest ./wine.nix {};
   wireguard = handleTest ./wireguard {};
@@ -726,6 +808,7 @@ in {
   zigbee2mqtt = handleTest ./zigbee2mqtt.nix {};
   zoneminder = handleTest ./zoneminder.nix {};
   zookeeper = handleTest ./zookeeper.nix {};
+  zram-generator = handleTest ./zram-generator.nix {};
   zrepl = handleTest ./zrepl.nix {};
   zsh-history = handleTest ./zsh-history.nix {};
 }
diff --git a/nixos/tests/apcupsd.nix b/nixos/tests/apcupsd.nix
new file mode 100644
index 00000000000..287140f039d
--- /dev/null
+++ b/nixos/tests/apcupsd.nix
@@ -0,0 +1,41 @@
+let
+  # arbitrary address
+  ipAddr = "192.168.42.42";
+in
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "apcupsd";
+  meta.maintainers = with lib.maintainers; [ bjornfor ];
+
+  nodes = {
+    machine = {
+      services.apcupsd = {
+        enable = true;
+        configText = ''
+          UPSTYPE usb
+          BATTERYLEVEL 42
+          # Configure NISIP so that the only way apcaccess can work is to read
+          # this config.
+          NISIP ${ipAddr}
+        '';
+      };
+      networking.interfaces.eth1 = {
+        ipv4.addresses = [{
+          address = ipAddr;
+          prefixLength = 24;
+        }];
+      };
+    };
+  };
+
+  # Check that the service starts, that the CLI (apcaccess) works and that it
+  # uses the config (ipAddr) defined in the service config.
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("apcupsd.service")
+    machine.wait_for_open_port(3551, "${ipAddr}")
+    res = machine.succeed("apcaccess")
+    expect_line="MBATTCHG : 42 Percent"
+    assert "MBATTCHG : 42 Percent" in res, f"expected apcaccess output to contain '{expect_line}' but got '{res}'"
+    machine.shutdown()
+  '';
+})
diff --git a/nixos/tests/apfs.nix b/nixos/tests/apfs.nix
index a8841fe9304..ac0459b57e9 100644
--- a/nixos/tests/apfs.nix
+++ b/nixos/tests/apfs.nix
@@ -21,9 +21,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     with subtest("Enable case sensitivity and normalization sensitivity"):
       machine.succeed(
           "mkapfs -s -z /dev/vdb",
-          # Triggers a bug, see https://github.com/linux-apfs/linux-apfs-rw/issues/15
-          # "mount -o cknodes,readwrite /dev/vdb /tmp/mnt",
-          "mount -o readwrite /dev/vdb /tmp/mnt",
+          "mount -o cknodes,readwrite /dev/vdb /tmp/mnt",
           "echo 'Hello World 1' > /tmp/mnt/test.txt",
           "[ ! -f /tmp/mnt/TeSt.TxT ] || false", # Test case sensitivity
           "echo 'Hello World 1' | diff - /tmp/mnt/test.txt",
@@ -36,13 +34,13 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     with subtest("Disable case sensitivity and normalization sensitivity"):
       machine.succeed(
           "mkapfs /dev/vdb",
-          "mount -o readwrite /dev/vdb /tmp/mnt",
+          "mount -o cknodes,readwrite /dev/vdb /tmp/mnt",
           "echo 'bla bla bla' > /tmp/mnt/Test.txt",
           "echo -n 'Hello World' > /tmp/mnt/test.txt",
           "echo ' 1' >> /tmp/mnt/TEST.TXT",
           "umount /tmp/mnt",
           "apfsck /dev/vdb",
-          "mount -o readwrite /dev/vdb /tmp/mnt",
+          "mount -o cknodes,readwrite /dev/vdb /tmp/mnt",
           "echo 'Hello World 1' | diff - /tmp/mnt/TeSt.TxT", # Test case insensitivity
           "echo 'Hello World 2' > /tmp/mnt/\u0061\u0301.txt",
           "echo 'Hello World 2' | diff - /tmp/mnt/\u0061\u0301.txt",
diff --git a/nixos/tests/atuin.nix b/nixos/tests/atuin.nix
index 85213d1e53e..2bc5494f555 100644
--- a/nixos/tests/atuin.nix
+++ b/nixos/tests/atuin.nix
@@ -54,7 +54,7 @@ with lib;
     client.execute("echo 'sync_address = \"http://server:${toString testPort}\"' > ~/.config/atuin/config.toml")
 
     # log in to atuin server on client node
-    client.succeed(f"${atuin}/bin/atuin login -u ${testUser} -p ${testPass} -k {key}")
+    client.succeed(f"${atuin}/bin/atuin login -u ${testUser} -p ${testPass} -k \"{key}\"")
 
     # pull records from atuin server
     client.succeed("${atuin}/bin/atuin sync -f")
diff --git a/nixos/tests/authelia.nix b/nixos/tests/authelia.nix
new file mode 100644
index 00000000000..679c65fea08
--- /dev/null
+++ b/nixos/tests/authelia.nix
@@ -0,0 +1,169 @@
+# Test Authelia as an auth server for Traefik as a reverse proxy of a local web service
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "authelia";
+  meta.maintainers = with lib.maintainers; [ jk ];
+
+  nodes = {
+    authelia = { config, pkgs, lib, ... }: {
+      services.authelia.instances.testing = {
+        enable = true;
+        secrets.storageEncryptionKeyFile = "/etc/authelia/storageEncryptionKeyFile";
+        secrets.jwtSecretFile = "/etc/authelia/jwtSecretFile";
+        settings = {
+          authentication_backend.file.path = "/etc/authelia/users_database.yml";
+          access_control.default_policy = "one_factor";
+          session.domain = "example.com";
+          storage.local.path = "/tmp/db.sqlite3";
+          notifier.filesystem.filename = "/tmp/notifications.txt";
+        };
+      };
+
+      # These should not be set from nix but through other means to not leak the secret!
+      # This is purely for testing purposes!
+      environment.etc."authelia/storageEncryptionKeyFile" = {
+        mode = "0400";
+        user = "authelia-testing";
+        text = "you_must_generate_a_random_string_of_more_than_twenty_chars_and_configure_this";
+      };
+      environment.etc."authelia/jwtSecretFile" = {
+        mode = "0400";
+        user = "authelia-testing";
+        text = "a_very_important_secret";
+      };
+      environment.etc."authelia/users_database.yml" = {
+        mode = "0400";
+        user = "authelia-testing";
+        text = ''
+          users:
+            bob:
+              disabled: false
+              displayname: bob
+              # password of password
+              password: $argon2id$v=19$m=65536,t=3,p=4$2ohUAfh9yetl+utr4tLcCQ$AsXx0VlwjvNnCsa70u4HKZvFkC8Gwajr2pHGKcND/xs
+              email: bob@jim.com
+              groups:
+                - admin
+                - dev
+        '';
+      };
+
+      services.traefik = {
+        enable = true;
+
+        dynamicConfigOptions = {
+          tls.certificates =
+            let
+              certDir = pkgs.runCommand "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } ''
+                openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=example.com/CN=auth.example.com/CN=static.example.com' -days 36500
+                mkdir -p $out
+                cp key.pem cert.pem $out
+              '';
+            in
+            [{
+              certFile = "${certDir}/cert.pem";
+              keyFile = "${certDir}/key.pem";
+            }];
+          http.middlewares.authelia.forwardAuth = {
+            address = "http://localhost:9091/api/verify?rd=https%3A%2F%2Fauth.example.com%2F";
+            trustForwardHeader = true;
+            authResponseHeaders = [
+              "Remote-User"
+              "Remote-Groups"
+              "Remote-Email"
+              "Remote-Name"
+            ];
+          };
+          http.middlewares.authelia-basic.forwardAuth = {
+            address = "http://localhost:9091/api/verify?auth=basic";
+            trustForwardHeader = true;
+            authResponseHeaders = [
+              "Remote-User"
+              "Remote-Groups"
+              "Remote-Email"
+              "Remote-Name"
+            ];
+          };
+
+          http.routers.simplehttp = {
+            rule = "Host(`static.example.com`)";
+            tls = true;
+            entryPoints = "web";
+            service = "simplehttp";
+          };
+          http.routers.simplehttp-basic-auth = {
+            rule = "Host(`static-basic-auth.example.com`)";
+            tls = true;
+            entryPoints = "web";
+            service = "simplehttp";
+            middlewares = [ "authelia-basic@file" ];
+          };
+
+          http.services.simplehttp = {
+            loadBalancer.servers = [{
+              url = "http://localhost:8000";
+            }];
+          };
+
+          http.routers.authelia = {
+            rule = "Host(`auth.example.com`)";
+            tls = true;
+            entryPoints = "web";
+            service = "authelia@file";
+          };
+
+          http.services.authelia = {
+            loadBalancer.servers = [{
+              url = "http://localhost:9091";
+            }];
+          };
+        };
+
+        staticConfigOptions = {
+          global = {
+            checkNewVersion = false;
+            sendAnonymousUsage = false;
+          };
+
+          entryPoints.web.address = ":443";
+        };
+      };
+
+      systemd.services.simplehttp =
+        let fakeWebPageDir = pkgs.writeTextDir "index.html" "hello"; in
+        {
+          script = "${pkgs.python3}/bin/python -m http.server --directory ${fakeWebPageDir} 8000";
+          serviceConfig.Type = "simple";
+          wantedBy = [ "multi-user.target" ];
+        };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    authelia.wait_for_unit("simplehttp.service")
+    authelia.wait_for_unit("traefik.service")
+    authelia.wait_for_unit("authelia-testing.service")
+    authelia.wait_for_open_port(443)
+    authelia.wait_for_unit("multi-user.target")
+
+    with subtest("Check for authelia"):
+      # expect the login page
+      assert "Login - Authelia", "could not reach authelia" in \
+        authelia.succeed("curl --insecure -sSf -H Host:auth.example.com https://authelia:443/")
+
+    with subtest("Check contacting basic http server via traefik with https works"):
+      assert "hello", "could not reach raw static site" in \
+        authelia.succeed("curl --insecure -sSf -H Host:static.example.com https://authelia:443/")
+
+    with subtest("Test traefik and authelia"):
+      with subtest("No details fail"):
+        authelia.fail("curl --insecure -sSf -H Host:static-basic-auth.example.com https://authelia:443/")
+      with subtest("Incorrect details fail"):
+        authelia.fail("curl --insecure -sSf -u 'bob:wordpass' -H Host:static-basic-auth.example.com https://authelia:443/")
+        authelia.fail("curl --insecure -sSf -u 'alice:password' -H Host:static-basic-auth.example.com https://authelia:443/")
+      with subtest("Correct details pass"):
+        assert "hello", "could not reach authed static site with valid credentials" in \
+          authelia.succeed("curl --insecure -sSf -u 'bob:password' -H Host:static-basic-auth.example.com https://authelia:443/")
+  '';
+})
diff --git a/nixos/tests/binary-cache.nix b/nixos/tests/binary-cache.nix
new file mode 100644
index 00000000000..0809e59e5a1
--- /dev/null
+++ b/nixos/tests/binary-cache.nix
@@ -0,0 +1,62 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "binary-cache";
+  meta.maintainers = with maintainers; [ thomasjm ];
+
+  nodes.machine =
+    { pkgs, ... }: {
+      imports = [ ../modules/installer/cd-dvd/channel.nix ];
+      environment.systemPackages = with pkgs; [python3];
+      system.extraDependencies = with pkgs; [hello.inputDerivation];
+      nix.extraOptions = ''
+        experimental-features = nix-command
+      '';
+    };
+
+  testScript = ''
+    # Build the cache, then remove it from the store
+    cachePath = machine.succeed("nix-build --no-out-link -E 'with import <nixpkgs> {}; mkBinaryCache { rootPaths = [hello]; }'").strip()
+    machine.succeed("cp -r %s/. /tmp/cache" % cachePath)
+    machine.succeed("nix-store --delete " + cachePath)
+
+    # Sanity test of cache structure
+    status, stdout = machine.execute("ls /tmp/cache")
+    cache_files = stdout.split()
+    assert ("nix-cache-info" in cache_files)
+    assert ("nar" in cache_files)
+
+    # Nix store ping should work
+    machine.succeed("nix store ping --store file:///tmp/cache")
+
+    # Cache should contain a .narinfo referring to "hello"
+    grepLogs = machine.succeed("grep -l 'StorePath: /nix/store/[[:alnum:]]*-hello-.*' /tmp/cache/*.narinfo")
+
+    # Get the store path referenced by the .narinfo
+    narInfoFile = grepLogs.strip()
+    narInfoContents = machine.succeed("cat " + narInfoFile)
+    import re
+    match = re.match(r"^StorePath: (/nix/store/[a-z0-9]*-hello-.*)$", narInfoContents, re.MULTILINE)
+    if not match: raise Exception("Couldn't find hello store path in cache")
+    storePath = match[1]
+
+    # Delete the store path
+    machine.succeed("nix-store --delete " + storePath)
+    machine.succeed("[ ! -d %s ] || exit 1" % storePath)
+
+    # Should be able to build hello using the cache
+    logs = machine.succeed("nix-build -A hello '<nixpkgs>' --option require-sigs false --option trusted-substituters file:///tmp/cache --option substituters file:///tmp/cache 2>&1")
+    logLines = logs.split("\n")
+    if not "this path will be fetched" in logLines[0]: raise Exception("Unexpected first log line")
+    def shouldBe(got, desired):
+      if got != desired: raise Exception("Expected '%s' but got '%s'" % (desired, got))
+    shouldBe(logLines[1], "  " + storePath)
+    shouldBe(logLines[2], "copying path '%s' from 'file:///tmp/cache'..." % storePath)
+    shouldBe(logLines[3], storePath)
+
+    # Store path should exist in the store now
+    machine.succeed("[ -d %s ] || exit 1" % storePath)
+  '';
+})
diff --git a/nixos/tests/bootspec.nix b/nixos/tests/bootspec.nix
index 13360bb1eaa..9295500422a 100644
--- a/nixos/tests/bootspec.nix
+++ b/nixos/tests/bootspec.nix
@@ -43,7 +43,7 @@ in
       machine.start()
       machine.wait_for_unit("multi-user.target")
 
-      machine.succeed("test -e /run/current-system/bootspec/boot.json")
+      machine.succeed("test -e /run/current-system/boot.json")
     '';
   };
 
@@ -65,7 +65,7 @@ in
       machine.start()
       machine.wait_for_unit("multi-user.target")
 
-      machine.succeed("test -e /run/current-system/bootspec/boot.json")
+      machine.succeed("test -e /run/current-system/boot.json")
     '';
   };
 
@@ -86,7 +86,33 @@ in
       machine.start()
       machine.wait_for_unit("multi-user.target")
 
-      machine.succeed("test -e /run/current-system/bootspec/boot.json")
+      machine.succeed("test -e /run/current-system/boot.json")
+    '';
+  };
+
+  # Check that initrd create corresponding entries in bootspec.
+  initrd = makeTest {
+    name = "bootspec-with-initrd";
+    meta.maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+
+    nodes.machine = {
+      imports = [ standard ];
+      environment.systemPackages = [ pkgs.jq ];
+      # It's probably the case, but we want to make it explicit here.
+      boot.initrd.enable = true;
+    };
+
+    testScript = ''
+      import json
+
+      machine.start()
+      machine.wait_for_unit("multi-user.target")
+
+      machine.succeed("test -e /run/current-system/boot.json")
+
+      bootspec = json.loads(machine.succeed("jq -r '.\"org.nixos.bootspec.v1\"' /run/current-system/boot.json"))
+
+      assert all(key in bootspec for key in ('initrd', 'initrdSecrets')), "Bootspec should contain initrd or initrdSecrets field when initrd is enabled"
     '';
   };
 
@@ -107,13 +133,13 @@ in
       machine.start()
       machine.wait_for_unit("multi-user.target")
 
-      machine.succeed("test -e /run/current-system/bootspec/boot.json")
-      machine.succeed("test -e /run/current-system/specialisation/something/bootspec/boot.json")
+      machine.succeed("test -e /run/current-system/boot.json")
+      machine.succeed("test -e /run/current-system/specialisation/something/boot.json")
 
-      sp_in_parent = json.loads(machine.succeed("jq -r '.v1.specialisation.something' /run/current-system/bootspec/boot.json"))
-      sp_in_fs = json.loads(machine.succeed("cat /run/current-system/specialisation/something/bootspec/boot.json"))
+      sp_in_parent = json.loads(machine.succeed("jq -r '.\"org.nixos.specialisation.v1\".something' /run/current-system/boot.json"))
+      sp_in_fs = json.loads(machine.succeed("cat /run/current-system/specialisation/something/boot.json"))
 
-      assert sp_in_parent == sp_in_fs['v1'], "Bootspecs of the same specialisation are different!"
+      assert sp_in_parent['org.nixos.bootspec.v1'] == sp_in_fs['org.nixos.bootspec.v1'], "Bootspecs of the same specialisation are different!"
     '';
   };
 
@@ -126,7 +152,9 @@ in
       imports = [ standard ];
       environment.systemPackages = [ pkgs.jq ];
       boot.bootspec.extensions = {
-        osRelease = config.environment.etc."os-release".source;
+        "org.nix-tests.product" = {
+          osRelease = config.environment.etc."os-release".source;
+        };
       };
     };
 
@@ -135,7 +163,7 @@ in
       machine.wait_for_unit("multi-user.target")
 
       current_os_release = machine.succeed("cat /etc/os-release")
-      bootspec_os_release = machine.succeed("cat $(jq -r '.v1.extensions.osRelease' /run/current-system/bootspec/boot.json)")
+      bootspec_os_release = machine.succeed("cat $(jq -r '.\"org.nix-tests.product\".osRelease' /run/current-system/boot.json)")
 
       assert current_os_release == bootspec_os_release, "Filename referenced by extension has unexpected contents"
     '';
diff --git a/nixos/tests/borgbackup.nix b/nixos/tests/borgbackup.nix
index 9afe4d537da..4160e727f04 100644
--- a/nixos/tests/borgbackup.nix
+++ b/nixos/tests/borgbackup.nix
@@ -117,8 +117,10 @@ in {
     server = { ... }: {
       services.openssh = {
         enable = true;
-        passwordAuthentication = false;
-        kbdInteractiveAuthentication = false;
+        settings = {
+          PasswordAuthentication = false;
+          KbdInteractiveAuthentication = false;
+        };
       };
 
       services.borgbackup.repos.repo1 = {
diff --git a/nixos/tests/bpf.nix b/nixos/tests/bpf.nix
index 5868e3bfcb4..5dc97404772 100644
--- a/nixos/tests/bpf.nix
+++ b/nixos/tests/bpf.nix
@@ -25,5 +25,9 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     print(machine.succeed("bpftrace -e 'kprobe:schedule { "
         "    printf(\"tgid: %d\", ((struct task_struct*) curtask)->tgid); exit() "
         "}'"))
+    # module BTF (bpftrace >= 0.17)
+    print(machine.succeed("bpftrace -e 'kfunc:nft_trans_alloc_gfp { "
+        "    printf(\"portid: %d\\n\",args->ctx->portid); "
+        "} BEGIN { exit() }'"))
   '';
 })
diff --git a/nixos/tests/btrbk-doas.nix b/nixos/tests/btrbk-doas.nix
new file mode 100644
index 00000000000..1e3f8d56add
--- /dev/null
+++ b/nixos/tests/btrbk-doas.nix
@@ -0,0 +1,114 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+
+  let
+    privateKey = ''
+      -----BEGIN OPENSSH PRIVATE KEY-----
+      b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+      QyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrwAAAJB+cF5HfnBe
+      RwAAAAtzc2gtZWQyNTUxOQAAACBx8UB04Q6Q/fwDFjakHq904PYFzG9pU2TJ9KXpaPMcrw
+      AAAEBN75NsJZSpt63faCuaD75Unko0JjlSDxMhYHAPJk2/xXHxQHThDpD9/AMWNqQer3Tg
+      9gXMb2lTZMn0pelo8xyvAAAADXJzY2h1ZXR6QGt1cnQ=
+      -----END OPENSSH PRIVATE KEY-----
+    '';
+    publicKey = ''
+      ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHxQHThDpD9/AMWNqQer3Tg9gXMb2lTZMn0pelo8xyv
+    '';
+  in
+  {
+    name = "btrbk-doas";
+    meta = with pkgs.lib; {
+      maintainers = with maintainers; [ symphorien tu-maurice ];
+    };
+
+    nodes = {
+      archive = { ... }: {
+        security.sudo.enable = false;
+        security.doas.enable = true;
+        environment.systemPackages = with pkgs; [ btrfs-progs ];
+        # note: this makes the privateKey world readable.
+        # don't do it with real ssh keys.
+        environment.etc."btrbk_key".text = privateKey;
+        services.btrbk = {
+          extraPackages = [ pkgs.lz4 ];
+          instances = {
+            remote = {
+              onCalendar = "minutely";
+              settings = {
+                ssh_identity = "/etc/btrbk_key";
+                ssh_user = "btrbk";
+                stream_compress = "lz4";
+                volume = {
+                  "ssh://main/mnt" = {
+                    target = "/mnt";
+                    snapshot_dir = "btrbk/remote";
+                    subvolume = "to_backup";
+                  };
+                };
+              };
+            };
+          };
+        };
+      };
+
+      main = { ... }: {
+        security.sudo.enable = false;
+        security.doas.enable = true;
+        environment.systemPackages = with pkgs; [ btrfs-progs ];
+        services.openssh = {
+          enable = true;
+          passwordAuthentication = false;
+          kbdInteractiveAuthentication = false;
+        };
+        services.btrbk = {
+          extraPackages = [ pkgs.lz4 ];
+          sshAccess = [
+            {
+              key = publicKey;
+              roles = [ "source" "send" "info" "delete" ];
+            }
+          ];
+          instances = {
+            local = {
+              onCalendar = "minutely";
+              settings = {
+                volume = {
+                  "/mnt" = {
+                    snapshot_dir = "btrbk/local";
+                    subvolume = "to_backup";
+                  };
+                };
+              };
+            };
+          };
+        };
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      # create btrfs partition at /mnt
+      for machine in (archive, main):
+        machine.succeed("dd if=/dev/zero of=/data_fs bs=120M count=1")
+        machine.succeed("mkfs.btrfs /data_fs")
+        machine.succeed("mkdir /mnt")
+        machine.succeed("mount /data_fs /mnt")
+
+      # what to backup and where
+      main.succeed("btrfs subvolume create /mnt/to_backup")
+      main.succeed("mkdir -p /mnt/btrbk/{local,remote}")
+
+      # check that local snapshots work
+      with subtest("local"):
+          main.succeed("echo foo > /mnt/to_backup/bar")
+          main.wait_until_succeeds("cat /mnt/btrbk/local/*/bar | grep foo")
+          main.succeed("echo bar > /mnt/to_backup/bar")
+          main.succeed("cat /mnt/btrbk/local/*/bar | grep foo")
+
+      # check that btrfs send/receive works and ssh access works
+      with subtest("remote"):
+          archive.wait_until_succeeds("cat /mnt/*/bar | grep bar")
+          main.succeed("echo baz > /mnt/to_backup/bar")
+          archive.succeed("cat /mnt/*/bar | grep bar")
+    '';
+  })
diff --git a/nixos/tests/btrbk.nix b/nixos/tests/btrbk.nix
index 9f34f7dfbe3..5261321dfa2 100644
--- a/nixos/tests/btrbk.nix
+++ b/nixos/tests/btrbk.nix
@@ -52,8 +52,10 @@ import ./make-test-python.nix ({ pkgs, ... }:
         environment.systemPackages = with pkgs; [ btrfs-progs ];
         services.openssh = {
           enable = true;
-          passwordAuthentication = false;
-          kbdInteractiveAuthentication = false;
+          settings = {
+            KbdInteractiveAuthentication = false;
+            PasswordAuthentication = false;
+          };
         };
         services.btrbk = {
           extraPackages = [ pkgs.lz4 ];
diff --git a/nixos/tests/budgie.nix b/nixos/tests/budgie.nix
new file mode 100644
index 00000000000..a2599572b3b
--- /dev/null
+++ b/nixos/tests/budgie.nix
@@ -0,0 +1,56 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "budgie";
+
+  meta = with lib; {
+    maintainers = [ maintainers.federicoschonborn ];
+  };
+
+  nodes.machine = { ... }: {
+    imports = [
+      ./common/user-account.nix
+    ];
+
+    services.xserver.enable = true;
+
+    services.xserver.displayManager = {
+      lightdm.enable = true;
+      autoLogin = {
+        enable = true;
+        user = "alice";
+      };
+    };
+
+    services.xserver.desktopManager.budgie = {
+      enable = true;
+      extraPlugins = [
+        pkgs.budgie.budgie-analogue-clock-applet
+      ];
+    };
+  };
+
+  testScript = { nodes, ... }:
+    let
+      user = nodes.machine.users.users.alice;
+    in
+    ''
+      with subtest("Wait for login"):
+          machine.wait_for_x()
+          machine.wait_for_file("${user.home}/.Xauthority")
+          machine.succeed("xauth merge ${user.home}/.Xauthority")
+
+      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("Check if Budgie session components actually start"):
+          machine.wait_until_succeeds("pgrep budgie-daemon")
+          machine.wait_for_window("budgie-daemon")
+          machine.wait_until_succeeds("pgrep budgie-panel")
+          machine.wait_for_window("budgie-panel")
+
+      with subtest("Open MATE terminal"):
+          machine.succeed("su - ${user.name} -c 'DISPLAY=:0 mate-terminal >&2 &'")
+          machine.wait_for_window("Terminal")
+          machine.sleep(20)
+          machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/buildbot.nix b/nixos/tests/buildbot.nix
index 977c728835f..467c8d8baff 100644
--- a/nixos/tests/buildbot.nix
+++ b/nixos/tests/buildbot.nix
@@ -23,7 +23,7 @@ import ./make-test-python.nix {
         ];
       };
       networking.firewall.allowedTCPPorts = [ 8010 8011 9989 ];
-      environment.systemPackages = with pkgs; [ git python3Packages.buildbot-full ];
+      environment.systemPackages = with pkgs; [ git buildbot-full ];
     };
 
     bbworker = { pkgs, ... }: {
@@ -31,7 +31,7 @@ import ./make-test-python.nix {
         enable = true;
         masterUrl = "bbmaster:9989";
       };
-      environment.systemPackages = with pkgs; [ git python3Packages.buildbot-worker ];
+      environment.systemPackages = with pkgs; [ git buildbot-worker ];
     };
 
     gitrepo = { pkgs, ... }: {
diff --git a/nixos/tests/cage.nix b/nixos/tests/cage.nix
index 39c8d0441b6..db1b7854673 100644
--- a/nixos/tests/cage.nix
+++ b/nixos/tests/cage.nix
@@ -10,11 +10,13 @@ import ./make-test-python.nix ({ pkgs, ...} :
 
   {
     imports = [ ./common/user-account.nix ];
+
+    fonts.fonts = with pkgs; [ dejavu_fonts ];
+
     services.cage = {
       enable = true;
       user = "alice";
-      # Disable color and bold and use a larger font to make OCR easier:
-      program = "${pkgs.xterm}/bin/xterm -cm -pc -fa Monospace -fs 24";
+      program = "${pkgs.xterm}/bin/xterm";
     };
 
     # Need to switch to a different GPU driver than the default one (-vga std) so that Cage can launch:
diff --git a/nixos/tests/ceph-single-node.nix b/nixos/tests/ceph-single-node.nix
index 4fe5dc59ff8..4a5636fac15 100644
--- a/nixos/tests/ceph-single-node.nix
+++ b/nixos/tests/ceph-single-node.nix
@@ -181,6 +181,17 @@ let
     monA.wait_until_succeeds("ceph osd stat | grep -e '3 osds: 3 up[^,]*, 3 in'")
     monA.wait_until_succeeds("ceph -s | grep 'mgr: ${cfg.monA.name}(active,'")
     monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
+
+    # Enable the dashboard and recheck health
+    monA.succeed(
+        "ceph mgr module enable dashboard",
+        "ceph config set mgr mgr/dashboard/ssl false",
+        # default is 8080 but it's better to be explicit
+        "ceph config set mgr mgr/dashboard/server_port 8080",
+    )
+    monA.wait_for_open_port(8080)
+    monA.wait_until_succeeds("curl -q --fail http://localhost:8080")
+    monA.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
   '';
 in {
   name = "basic-single-node-ceph-cluster";
diff --git a/nixos/tests/cgit.nix b/nixos/tests/cgit.nix
new file mode 100644
index 00000000000..6aed06adefd
--- /dev/null
+++ b/nixos/tests/cgit.nix
@@ -0,0 +1,73 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+  robotsTxt = pkgs.writeText "cgit-robots.txt" ''
+    User-agent: *
+    Disallow: /
+  '';
+in {
+  name = "cgit";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ schnusch ];
+  };
+
+  nodes = {
+    server = { ... }: {
+      services.cgit."localhost" = {
+        enable = true;
+        package = pkgs.cgit.overrideAttrs ({ postInstall, ... }: {
+          postInstall = ''
+            ${postInstall}
+            cp ${robotsTxt} "$out/cgit/robots.txt"
+          '';
+        });
+        nginx.location = "/(c)git/";
+        repos = {
+          some-repo = {
+            path = "/srv/git/some-repo";
+            desc = "some-repo description";
+          };
+        };
+      };
+
+      environment.systemPackages = [ pkgs.git ];
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    start_all()
+
+    server.wait_for_unit("nginx.service")
+    server.wait_for_unit("network.target")
+    server.wait_for_open_port(80)
+
+    server.succeed("curl -fsS http://localhost/%28c%29git/cgit.css")
+
+    server.succeed("curl -fsS http://localhost/%28c%29git/robots.txt | diff -u - ${robotsTxt}")
+
+    server.succeed(
+        "curl -fsS http://localhost/%28c%29git/ | grep -F 'some-repo description'"
+    )
+
+    server.fail("curl -fsS http://localhost/robots.txt")
+
+    server.succeed("${pkgs.writeShellScript "setup-cgit-test-repo" ''
+      set -e
+      git init --bare -b master /srv/git/some-repo
+      git init -b master reference
+      cd reference
+      git remote add origin /srv/git/some-repo
+      date > date.txt
+      git add date.txt
+      git -c user.name=test -c user.email=test@localhost commit -m 'add date'
+      git push -u origin master
+    ''}")
+
+    server.succeed(
+        "curl -fsS 'http://localhost/%28c%29git/some-repo/plain/date.txt?id=master' | diff -u reference/date.txt -"
+    )
+
+    server.succeed(
+       "git clone http://localhost/%28c%29git/some-repo && diff -u reference/date.txt some-repo/date.txt"
+    )
+  '';
+})
diff --git a/nixos/tests/chrony-ptp.nix b/nixos/tests/chrony-ptp.nix
new file mode 100644
index 00000000000..b2634a8cfc5
--- /dev/null
+++ b/nixos/tests/chrony-ptp.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ lib, ... }:
+{
+  name = "chrony-ptp";
+
+  meta = {
+    maintainers = with lib.maintainers; [ gkleen ];
+  };
+
+  nodes = {
+    qemuGuest = { lib, ... }: {
+      boot.kernelModules = [ "ptp_kvm" ];
+
+      services.chrony = {
+        enable = true;
+        extraConfig = ''
+          refclock PHC /dev/ptp_kvm poll 2 dpoll -2 offset 0 stratum 3
+        '';
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    qemuGuest.wait_for_unit('multi-user.target')
+    qemuGuest.succeed('systemctl is-active chronyd.service')
+  '';
+})
diff --git a/nixos/tests/clickhouse.nix b/nixos/tests/clickhouse.nix
index 043263ec05d..77d6a7ab8be 100644
--- a/nixos/tests/clickhouse.nix
+++ b/nixos/tests/clickhouse.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "clickhouse";
-  meta.maintainers = with pkgs.lib.maintainers; [ ma27 ];
+  meta.maintainers = with pkgs.lib.maintainers; [ ];
 
   nodes.machine = {
     services.clickhouse.enable = true;
diff --git a/nixos/tests/cloudlog.nix b/nixos/tests/cloudlog.nix
new file mode 100644
index 00000000000..c99951c1b22
--- /dev/null
+++ b/nixos/tests/cloudlog.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "cloudlog";
+  meta = {
+    maintainers = with pkgs.lib.maintainers; [ melling ];
+  };
+  nodes = {
+    machine = {
+      services.mysql.package = pkgs.mariadb;
+      services.cloudlog.enable = true;
+    };
+  };
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("phpfpm-cloudlog")
+    machine.wait_for_open_port(80);
+    machine.wait_until_succeeds("curl -s -L --fail http://localhost | grep 'Login - Cloudlog'")
+  '';
+})
diff --git a/nixos/tests/cockpit.nix b/nixos/tests/cockpit.nix
new file mode 100644
index 00000000000..4a4983f9bc4
--- /dev/null
+++ b/nixos/tests/cockpit.nix
@@ -0,0 +1,135 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+
+  let
+    user = "alice"; # from ./common/user-account.nix
+    password = "foobar"; # from ./common/user-account.nix
+  in {
+    name = "cockpit";
+    meta = {
+      maintainers = with lib.maintainers; [ lucasew ];
+    };
+    nodes = {
+      server = { config, ... }: {
+        imports = [ ./common/user-account.nix ];
+        security.polkit.enable = true;
+        users.users.${user} = {
+          extraGroups = [ "wheel" ];
+        };
+        services.cockpit = {
+          enable = true;
+          openFirewall = true;
+          settings = {
+            WebService = {
+              Origins = "https://server:9090";
+            };
+          };
+        };
+      };
+      client = { config, ... }: {
+        imports = [ ./common/user-account.nix ];
+        environment.systemPackages = let
+          seleniumScript = pkgs.writers.writePython3Bin "selenium-script" {
+            libraries = with pkgs.python3Packages; [ selenium ];
+            } ''
+            from selenium import webdriver
+            from selenium.webdriver.common.by import By
+            from selenium.webdriver.firefox.options import Options
+            from selenium.webdriver.support.ui import WebDriverWait
+            from selenium.webdriver.support import expected_conditions as EC
+            from time import sleep
+
+
+            def log(msg):
+                from sys import stderr
+                print(f"[*] {msg}", file=stderr)
+
+
+            log("Initializing")
+
+            options = Options()
+            options.add_argument("--headless")
+
+            driver = webdriver.Firefox(options=options)
+
+            driver.implicitly_wait(10)
+
+            log("Opening homepage")
+            driver.get("https://server:9090")
+
+            wait = WebDriverWait(driver, 60)
+
+
+            def wait_elem(by, query):
+                wait.until(EC.presence_of_element_located((by, query)))
+
+
+            def wait_title_contains(title):
+                wait.until(EC.title_contains(title))
+
+
+            def find_element(by, query):
+                return driver.find_element(by, query)
+
+
+            def set_value(elem, value):
+                script = 'arguments[0].value = arguments[1]'
+                return driver.execute_script(script, elem, value)
+
+
+            log("Waiting for the homepage to load")
+
+            # cockpit sets initial title as hostname
+            wait_title_contains("server")
+            wait_elem(By.CSS_SELECTOR, 'input#login-user-input')
+
+            log("Homepage loaded!")
+
+            log("Filling out username")
+            login_input = find_element(By.CSS_SELECTOR, 'input#login-user-input')
+            set_value(login_input, "${user}")
+
+            log("Filling out password")
+            password_input = find_element(By.CSS_SELECTOR, 'input#login-password-input')
+            set_value(password_input, "${password}")
+
+            log("Submiting credentials for login")
+            driver.find_element(By.CSS_SELECTOR, 'button#login-button').click()
+
+            # driver.implicitly_wait(1)
+            # driver.get("https://server:9090/system")
+
+            log("Waiting dashboard to load")
+            wait_title_contains("${user}@server")
+
+            log("Waiting for the frontend to initalize")
+            sleep(1)
+
+            log("Looking for that banner that tells about limited access")
+            container_iframe = find_element(By.CSS_SELECTOR, 'iframe.container-frame')
+            driver.switch_to.frame(container_iframe)
+
+            assert "Web console is running in limited access mode" in driver.page_source
+
+            driver.close()
+          '';
+        in with pkgs; [ firefox-unwrapped geckodriver seleniumScript ];
+      };
+    };
+
+    testScript = ''
+      start_all()
+
+      server.wait_for_open_port(9090)
+      server.wait_for_unit("network.target")
+      server.wait_for_unit("multi-user.target")
+      server.systemctl("start", "polkit")
+
+      client.wait_for_unit("multi-user.target")
+
+      client.succeed("curl -k https://server:9090 -o /dev/stderr")
+      print(client.succeed("whoami"))
+      client.succeed('PYTHONUNBUFFERED=1 selenium-script')
+    '';
+  }
+)
diff --git a/nixos/tests/coder.nix b/nixos/tests/coder.nix
new file mode 100644
index 00000000000..12813827284
--- /dev/null
+++ b/nixos/tests/coder.nix
@@ -0,0 +1,24 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "coder";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ shyim ghuntley ];
+  };
+
+  nodes.machine =
+    { pkgs, ... }:
+    {
+      services.coder = {
+        enable = true;
+        accessUrl = "http://localhost:3000";
+      };
+    };
+
+  testScript = ''
+    machine.start()
+    machine.wait_for_unit("postgresql.service")
+    machine.wait_for_unit("coder.service")
+    machine.wait_for_open_port(3000)
+
+    machine.succeed("curl --fail http://localhost:3000")
+  '';
+})
diff --git a/nixos/tests/common/acme/server/generate-certs.nix b/nixos/tests/common/acme/server/generate-certs.nix
index 85c751c56ad..4f38ca309b0 100644
--- a/nixos/tests/common/acme/server/generate-certs.nix
+++ b/nixos/tests/common/acme/server/generate-certs.nix
@@ -15,7 +15,7 @@ in mkDerivation {
       sed -i 's_NotAfter: time.Now().AddDate(2, 0, 30),_NotAfter: time.Now().AddDate(20, 0, 0),_' main.go
     '';
   })) ];
-  phases = [ "buildPhase" "installPhase" ];
+  dontUnpack = true;
 
   buildPhase = ''
     minica \
diff --git a/nixos/tests/common/ec2.nix b/nixos/tests/common/ec2.nix
index 6ed420e0aae..1a64c464039 100644
--- a/nixos/tests/common/ec2.nix
+++ b/nixos/tests/common/ec2.nix
@@ -17,6 +17,7 @@ with pkgs.lib;
           ln -s ${pkgs.writeText "sshPublicKey" sshPublicKey} $out/1.0/meta-data/public-keys/0/openssh-key
         '';
       };
+      indentLines = str: concatLines (map (s: "  " + s) (splitString "\n" str));
     in makeTest {
       name = "ec2-" + name;
       nodes = {};
@@ -36,6 +37,8 @@ with pkgs.lib;
                 "create",
                 "-f",
                 "qcow2",
+                "-F",
+                "qcow2",
                 "-o",
                 "backing_file=${image}",
                 disk_image,
@@ -59,7 +62,11 @@ with pkgs.lib;
         )
 
         machine = create_machine({"startCommand": start_command})
-      '' + script;
+        try:
+      '' + indentLines script + ''
+        finally:
+          machine.shutdown()
+      '';
 
       inherit meta;
     };
diff --git a/nixos/tests/connman.nix b/nixos/tests/connman.nix
new file mode 100644
index 00000000000..348b2a895a6
--- /dev/null
+++ b/nixos/tests/connman.nix
@@ -0,0 +1,77 @@
+import ./make-test-python.nix ({ pkgs, lib, ...}:
+{
+  name = "connman";
+  meta = with lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  # Router running radvd on VLAN 1
+  nodes.router = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    virtualisation.vlans = [ 1 ];
+
+    boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = true;
+
+    networking = {
+      useDHCP = false;
+      interfaces.eth1.ipv6.addresses =
+        [ { address = "fd12::1"; prefixLength = 64; } ];
+    };
+
+    services.radvd = {
+      enable = true;
+      config = ''
+        interface eth1 {
+          AdvSendAdvert on;
+          AdvManagedFlag on;
+          AdvOtherConfigFlag on;
+          prefix fd12::/64 {
+            AdvAutonomous off;
+          };
+        };
+      '';
+    };
+  };
+
+  # Client running connman, connected to VLAN 1
+  nodes.client = { ... }: {
+    virtualisation.vlans = [ 1 ];
+
+    # add a virtual wlan interface
+    boot.kernelModules = [ "mac80211_hwsim" ];
+    boot.extraModprobeConfig = ''
+      options mac80211_hwsim radios=1
+    '';
+
+    # Note: the overrides are needed because the wifi is
+    # disabled with mkVMOverride in qemu-vm.nix.
+    services.connman.enable = lib.mkOverride 0 true;
+    services.connman.networkInterfaceBlacklist = [ "eth0" ];
+    networking.wireless.enable = lib.mkOverride 0 true;
+    networking.wireless.interfaces = [ "wlan0" ];
+  };
+
+  testScript =
+    ''
+      start_all()
+
+      with subtest("Router is ready"):
+          router.wait_for_unit("radvd.service")
+
+      with subtest("Daemons are running"):
+          client.wait_for_unit("wpa_supplicant-wlan0.service")
+          client.wait_for_unit("connman.service")
+          client.wait_until_succeeds("connmanctl state | grep -q ready")
+
+      with subtest("Wired interface is configured"):
+          client.wait_until_succeeds("ip -6 route | grep -q fd12::/64")
+          client.wait_until_succeeds("ping -c 1 fd12::1")
+
+      with subtest("Can set up a wireless access point"):
+          client.succeed("connmanctl enable wifi")
+          client.wait_until_succeeds("connmanctl tether wifi on nixos-test reproducibility | grep -q 'Enabled'")
+          client.wait_until_succeeds("iw wlan0 info | grep -q nixos-test")
+    '';
+})
+
diff --git a/nixos/tests/consul-template.nix b/nixos/tests/consul-template.nix
new file mode 100644
index 00000000000..cbffa94569e
--- /dev/null
+++ b/nixos/tests/consul-template.nix
@@ -0,0 +1,36 @@
+import ./make-test-python.nix ({ ... }: {
+  name = "consul-template";
+
+  nodes.machine = { ... }: {
+    services.consul-template.instances.example.settings = {
+      template = [{
+        contents = ''
+          {{ key "example" }}
+        '';
+        perms = "0600";
+        destination = "/example";
+      }];
+    };
+
+    services.consul = {
+      enable = true;
+      extraConfig = {
+        server = true;
+        bootstrap_expect = 1;
+        bind_addr = "127.0.0.1";
+      };
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("consul.service")
+    machine.wait_for_open_port(8500)
+
+    machine.wait_for_unit("consul-template-example.service")
+
+    machine.wait_until_succeeds('consul kv put example example')
+
+    machine.wait_for_file("/example")
+    machine.succeed('grep "example" /example')
+  '';
+})
diff --git a/nixos/tests/consul.nix b/nixos/tests/consul.nix
index ee85f1d0b91..6233234ff08 100644
--- a/nixos/tests/consul.nix
+++ b/nixos/tests/consul.nix
@@ -145,7 +145,7 @@ in {
     client2.succeed("[ $(consul kv get testkey) == 42 ]")
 
 
-    def rolling_reboot_test(proper_rolling_procedure=True):
+    def rolling_restart_test(proper_rolling_procedure=True):
         """
         Tests that the cluster can tolearate failures of any single server,
         following the recommended rolling upgrade procedure from
@@ -158,7 +158,13 @@ in {
         """
 
         for server in servers:
-            server.crash()
+            server.block()
+            server.systemctl("stop consul")
+
+            # Make sure the stopped peer is recognized as being down
+            client1.wait_until_succeeds(
+              f"[ $(consul members | grep {server.name} | grep -o -E 'failed|left' | wc -l) == 1 ]"
+            )
 
             # For each client, wait until they have connection again
             # using `kv get -recurse` before issuing commands.
@@ -170,8 +176,8 @@ in {
             client2.succeed("[ $(consul kv get testkey) == 43 ]")
             client2.succeed("consul kv delete testkey")
 
-            # Restart crashed machine.
-            server.start()
+            server.unblock()
+            server.systemctl("start consul")
 
             if proper_rolling_procedure:
                 # Wait for recovery.
@@ -197,10 +203,14 @@ in {
         """
 
         for server in servers:
-            server.crash()
+            server.block()
+            server.systemctl("stop --no-block consul")
 
         for server in servers:
-            server.start()
+            # --no-block is async, so ensure it has been stopped by now
+            server.wait_until_fails("systemctl is-active --quiet consul")
+            server.unblock()
+            server.systemctl("start consul")
 
         # Wait for recovery.
         wait_for_healthy_servers()
@@ -217,13 +227,13 @@ in {
 
     # Run the tests.
 
-    print("rolling_reboot_test()")
-    rolling_reboot_test()
+    print("rolling_restart_test()")
+    rolling_restart_test()
 
     print("all_servers_crash_simultaneously_test()")
     all_servers_crash_simultaneously_test()
 
-    print("rolling_reboot_test(proper_rolling_procedure=False)")
-    rolling_reboot_test(proper_rolling_procedure=False)
+    print("rolling_restart_test(proper_rolling_procedure=False)")
+    rolling_restart_test(proper_rolling_procedure=False)
   '';
 })
diff --git a/nixos/tests/coturn.nix b/nixos/tests/coturn.nix
index dff832281c7..301b34b0da7 100644
--- a/nixos/tests/coturn.nix
+++ b/nixos/tests/coturn.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ ... }: {
+import ./make-test-python.nix ({ pkgs, ... }: {
   name = "coturn";
   nodes = {
     default = {
@@ -25,5 +25,9 @@ import ./make-test-python.nix ({ ... }: {
       with subtest("works with static-auth-secret-file"):
           secretsfile.wait_for_unit("coturn.service")
           secretsfile.succeed("grep 'some-very-secret-string' /run/coturn/turnserver.cfg")
+          # Forbidden IP, fails:
+          secretsfile.fail("${pkgs.coturn}/bin/turnutils_uclient -W some-very-secret-string 127.0.0.1 -DgX -e 127.0.0.1 -n 1 -c -y")
+          # allowed-peer-ip, should succeed:
+          secretsfile.succeed("${pkgs.coturn}/bin/turnutils_uclient -W some-very-secret-string 192.168.1.2 -DgX -e 192.168.1.2 -n 1 -c -y")
     '';
 })
diff --git a/nixos/tests/cups-pdf.nix b/nixos/tests/cups-pdf.nix
new file mode 100644
index 00000000000..957b0296a75
--- /dev/null
+++ b/nixos/tests/cups-pdf.nix
@@ -0,0 +1,40 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "cups-pdf";
+
+  nodes.machine = { pkgs, ... }: {
+    imports = [ ./common/user-account.nix ];
+    environment.systemPackages = [ pkgs.poppler_utils ];
+    fonts.fonts = [ pkgs.dejavu_fonts ];  # yields more OCR-able pdf
+    services.printing.cups-pdf.enable = true;
+    services.printing.cups-pdf.instances = {
+      opt = {};
+      noopt.installPrinter = false;
+    };
+    hardware.printers.ensurePrinters = [{
+      name = "noopt";
+      model = "CUPS-PDF_noopt.ppd";
+      deviceUri = "cups-pdf:/noopt";
+    }];
+  };
+
+  # we cannot check the files with pdftotext, due to
+  # https://github.com/alexivkin/CUPS-PDF-to-PDF/issues/7
+  # we need `imagemagickBig` as it has ghostscript support
+
+  testScript = ''
+    from subprocess import run
+    machine.wait_for_unit("multi-user.target")
+    for name in ("opt", "noopt"):
+        text = f"test text {name}".upper()
+        machine.wait_until_succeeds(f"lpstat -v {name}")
+        machine.succeed(f"su - alice -c 'echo -e \"\n  {text}\" | lp -d {name}'")
+        # wait until the pdf files are completely produced and readable by alice
+        machine.wait_until_succeeds(f"su - alice -c 'pdfinfo /var/spool/cups-pdf-{name}/users/alice/*.pdf'")
+        machine.succeed(f"cp /var/spool/cups-pdf-{name}/users/alice/*.pdf /tmp/{name}.pdf")
+        machine.copy_from_vm(f"/tmp/{name}.pdf", "")
+        run(f"${pkgs.imagemagickBig}/bin/convert -density 300 $out/{name}.pdf $out/{name}.jpeg", shell=True, check=True)
+        assert text.encode() in run(f"${lib.getExe pkgs.tesseract} $out/{name}.jpeg stdout", shell=True, check=True, capture_output=True).stdout
+  '';
+
+  meta.maintainers = [ lib.maintainers.yarny ];
+})
diff --git a/nixos/tests/darling.nix b/nixos/tests/darling.nix
new file mode 100644
index 00000000000..5665b4c2ffe
--- /dev/null
+++ b/nixos/tests/darling.nix
@@ -0,0 +1,44 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  # Well, we _can_ cross-compile from Linux :)
+  hello = pkgs.runCommand "hello" {
+    sdk = "${pkgs.darling.sdk}/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk";
+    nativeBuildInputs = with pkgs.llvmPackages_14; [ clang-unwrapped lld ];
+    src = pkgs.writeText "hello.c" ''
+      #include <stdio.h>
+      int main() {
+        printf("Hello, Darling!\n");
+        return 0;
+      }
+    '';
+  } ''
+    clang \
+      -target x86_64-apple-darwin \
+      -fuse-ld=lld \
+      -nostdinc -nostdlib \
+      -mmacosx-version-min=10.15 \
+      --sysroot $sdk \
+      -isystem $sdk/usr/include \
+      -L $sdk/usr/lib -lSystem \
+      $src -o $out
+  '';
+in
+{
+  name = "darling";
+
+  meta.maintainers = with lib.maintainers; [ zhaofengli ];
+
+  nodes.machine = {
+    programs.darling.enable = true;
+  };
+
+  testScript = ''
+    start_all()
+
+    # Darling holds stdout until the server is shutdown
+    machine.succeed("darling ${hello} >hello.out")
+    machine.succeed("grep Hello hello.out")
+    machine.succeed("darling shutdown")
+  '';
+})
diff --git a/nixos/tests/deepin.nix b/nixos/tests/deepin.nix
new file mode 100644
index 00000000000..0fe93486b6c
--- /dev/null
+++ b/nixos/tests/deepin.nix
@@ -0,0 +1,57 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "deepin";
+
+  meta = with lib; {
+    maintainers = teams.deepin.members;
+  };
+
+  nodes.machine = { ... }: {
+    imports = [
+      ./common/user-account.nix
+    ];
+
+    services.xserver.enable = true;
+
+    services.xserver.displayManager = {
+      lightdm.enable = true;
+      autoLogin = {
+        enable = true;
+        user = "alice";
+      };
+    };
+
+    services.xserver.desktopManager.deepin.enable = true;
+  };
+
+  testScript = { nodes, ... }:
+    let
+      user = nodes.machine.users.users.alice;
+    in
+    ''
+      with subtest("Wait for login"):
+          machine.wait_for_x()
+          machine.wait_for_file("${user.home}/.Xauthority")
+          machine.succeed("xauth merge ${user.home}/.Xauthority")
+
+      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("Check if DDE wm chooser actually start"):
+          machine.wait_until_succeeds("pgrep -f dde-wm-chooser")
+          machine.wait_for_window("dde-wm-chooser")
+          machine.execute("pkill dde-wm-chooser")
+
+
+      with subtest("Check if Deepin session components actually start"):
+          machine.wait_until_succeeds("pgrep -f dde-session-daemon")
+          machine.wait_for_window("dde-session-daemon")
+          machine.wait_until_succeeds("pgrep -f dde-desktop")
+          machine.wait_for_window("dde-desktop")
+
+      with subtest("Open deepin-terminal"):
+          machine.succeed("su - ${user.name} -c 'DISPLAY=:0 deepin-terminal >&2 &'")
+          machine.wait_for_window("deepin-terminal")
+          machine.sleep(20)
+          machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/discourse.nix b/nixos/tests/discourse.nix
index 35ca083c6c4..c79ba41c2eb 100644
--- a/nixos/tests/discourse.nix
+++ b/nixos/tests/discourse.nix
@@ -40,7 +40,7 @@ import ./make-test-python.nix (
 
         networking.extraHosts = ''
           127.0.0.1 ${discourseDomain}
-          ${nodes.client.config.networking.primaryIPAddress} ${clientDomain}
+          ${nodes.client.networking.primaryIPAddress} ${clientDomain}
         '';
 
         services.postfix = {
@@ -90,7 +90,7 @@ import ./make-test-python.nix (
 
         networking.extraHosts = ''
           127.0.0.1 ${clientDomain}
-          ${nodes.discourse.config.networking.primaryIPAddress} ${discourseDomain}
+          ${nodes.discourse.networking.primaryIPAddress} ${discourseDomain}
         '';
 
         services.dovecot2 = {
@@ -178,8 +178,8 @@ import ./make-test-python.nix (
         discourse.wait_until_succeeds("curl -sS -f https://${discourseDomain}")
         discourse.succeed(
             "curl -sS -f https://${discourseDomain}/session/csrf -c cookie -b cookie -H 'Accept: application/json' | jq -r '\"X-CSRF-Token: \" + .csrf' > csrf_token",
-            "curl -sS -f https://${discourseDomain}/session -c cookie -b cookie -H @csrf_token -H 'Accept: application/json' -d 'login=${nodes.discourse.config.services.discourse.admin.username}' -d \"password=${adminPassword}\" | jq -e '.user.username == \"${nodes.discourse.config.services.discourse.admin.username}\"'",
-            "curl -sS -f https://${discourseDomain}/login -v -H 'Accept: application/json' -c cookie -b cookie 2>&1 | grep ${nodes.discourse.config.services.discourse.admin.username}",
+            "curl -sS -f https://${discourseDomain}/session -c cookie -b cookie -H @csrf_token -H 'Accept: application/json' -d 'login=${nodes.discourse.services.discourse.admin.username}' -d \"password=${adminPassword}\" | jq -e '.user.username == \"${nodes.discourse.services.discourse.admin.username}\"'",
+            "curl -sS -f https://${discourseDomain}/login -v -H 'Accept: application/json' -c cookie -b cookie 2>&1 | grep ${nodes.discourse.services.discourse.admin.username}",
         )
 
         client.wait_for_unit("postfix.service")
diff --git a/nixos/tests/docker-tools.nix b/nixos/tests/docker-tools.nix
index 98704ecb2fb..44b583ebcea 100644
--- a/nixos/tests/docker-tools.nix
+++ b/nixos/tests/docker-tools.nix
@@ -1,6 +1,52 @@
 # this test creates a simple GNU image with docker tools and sees if it executes
 
-import ./make-test-python.nix ({ pkgs, ... }: {
+import ./make-test-python.nix ({ pkgs, ... }:
+let
+  # nixpkgs#214434: dockerTools.buildImage fails to unpack base images
+  # containing duplicate layers when those duplicate tarballs
+  # appear under the manifest's 'Layers'. Docker can generate images
+  # like this even though dockerTools does not.
+  repeatedLayerTestImage =
+    let
+      # Rootfs diffs for layers 1 and 2 are identical (and empty)
+      layer1 = pkgs.dockerTools.buildImage {  name = "empty";  };
+      layer2 = layer1.overrideAttrs (_: { fromImage = layer1; });
+      repeatedRootfsDiffs = pkgs.runCommandNoCC "image-with-links.tar" {
+        nativeBuildInputs = [pkgs.jq];
+      } ''
+        mkdir contents
+        tar -xf "${layer2}" -C contents
+        cd contents
+        first_rootfs=$(jq -r '.[0].Layers[0]' manifest.json)
+        second_rootfs=$(jq -r '.[0].Layers[1]' manifest.json)
+        target_rootfs=$(sha256sum "$first_rootfs" | cut -d' ' -f 1).tar
+
+        # Replace duplicated rootfs diffs with symlinks to one tarball
+        chmod -R ug+w .
+        mv "$first_rootfs" "$target_rootfs"
+        rm "$second_rootfs"
+        ln -s "../$target_rootfs" "$first_rootfs"
+        ln -s "../$target_rootfs" "$second_rootfs"
+
+        # Update manifest's layers to use the symlinks' target
+        cat manifest.json | \
+        jq ".[0].Layers[0] = \"$target_rootfs\"" |
+        jq ".[0].Layers[1] = \"$target_rootfs\"" > manifest.json.new
+        mv manifest.json.new manifest.json
+
+        tar --sort=name --hard-dereference -cf $out .
+        '';
+    in pkgs.dockerTools.buildImage {
+      fromImage = repeatedRootfsDiffs;
+      name = "repeated-layer-test";
+      tag = "latest";
+      copyToRoot = pkgs.bash;
+      # A runAsRoot script is required to force previous layers to be unpacked
+      runAsRoot = ''
+        echo 'runAsRoot has run.'
+      '';
+    };
+in {
   name = "docker-tools";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ lnl7 roberth ];
@@ -221,6 +267,12 @@ import ./make-test-python.nix ({ pkgs, ... }: {
             "docker run --rm ${examples.layersUnpackOrder.imageName} cat /layer-order"
         )
 
+    with subtest("Ensure repeated base layers handled by buildImage"):
+        docker.succeed(
+            "docker load --input='${repeatedLayerTestImage}'",
+            "docker run --rm ${repeatedLayerTestImage.imageName} /bin/bash -c 'exit 0'"
+        )
+
     with subtest("Ensure environment variables are correctly inherited"):
         docker.succeed(
             "docker load --input='${examples.environmentVariables}'"
diff --git a/nixos/tests/dokuwiki.nix b/nixos/tests/dokuwiki.nix
index 67657e89f74..55908a11f3f 100644
--- a/nixos/tests/dokuwiki.nix
+++ b/nixos/tests/dokuwiki.nix
@@ -1,86 +1,101 @@
 import ./make-test-python.nix ({ pkgs, ... }:
 
 let
-  template-bootstrap3 = pkgs.stdenv.mkDerivation {
+  template-bootstrap3 = pkgs.stdenv.mkDerivation rec {
     name = "bootstrap3";
-    # Download the theme from the dokuwiki site
-    src = pkgs.fetchurl {
-      url = "https://github.com/giterlizzi/dokuwiki-template-bootstrap3/archive/v2019-05-22.zip";
-      sha256 = "4de5ff31d54dd61bbccaf092c9e74c1af3a4c53e07aa59f60457a8f00cfb23a6";
+    version = "2022-07-27";
+    src = pkgs.fetchFromGitHub {
+      owner = "giterlizzi";
+      repo = "dokuwiki-template-bootstrap3";
+      rev = "v${version}";
+      hash = "sha256-B3Yd4lxdwqfCnfmZdp+i/Mzwn/aEuZ0ovagDxuR6lxo=";
     };
-    # We need unzip to build this package
-    nativeBuildInputs = [ pkgs.unzip ];
-    # Installing simply means copying all files to the output directory
     installPhase = "mkdir -p $out; cp -R * $out/";
   };
 
 
-  # Let's package the icalevents plugin
-  plugin-icalevents = pkgs.stdenv.mkDerivation {
+  plugin-icalevents = pkgs.stdenv.mkDerivation rec {
     name = "icalevents";
-    # Download the plugin from the dokuwiki site
-    src = pkgs.fetchurl {
-      url = "https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/2017-06-16/dokuwiki-plugin-icalevents-2017-06-16.zip";
-      sha256 = "e40ed7dd6bbe7fe3363bbbecb4de481d5e42385b5a0f62f6a6ce6bf3a1f9dfa8";
+    version = "2017-06-16";
+    src = pkgs.fetchzip {
+      stripRoot = false;
+      url = "https://github.com/real-or-random/dokuwiki-plugin-icalevents/releases/download/${version}/dokuwiki-plugin-icalevents-${version}.zip";
+      hash = "sha256-IPs4+qgEfe8AAWevbcCM9PnyI0uoyamtWeg4rEb+9Wc=";
     };
-    # We need unzip to build this package
-    nativeBuildInputs = [ pkgs.unzip ];
-    sourceRoot = ".";
-    # Installing simply means copying all files to the output directory
     installPhase = "mkdir -p $out; cp -R * $out/";
   };
 
+  acronymsFile = pkgs.writeText "acronyms.local.conf" ''
+    r13y  reproducibility
+  '';
+
+  dwWithAcronyms = pkgs.dokuwiki.overrideAttrs (prev: {
+    installPhase = prev.installPhase or "" + ''
+      ln -sf ${acronymsFile} $out/share/dokuwiki/conf/acronyms.local.conf
+    '';
+  });
+
+  mkNode = webserver: { ... }: {
+    services.dokuwiki = {
+      inherit webserver;
+
+      sites = {
+        "site1.local" = {
+          templates = [ template-bootstrap3 ];
+          settings = {
+            useacl = false;
+            userewrite = true;
+            template = "bootstrap3";
+          };
+        };
+        "site2.local" = {
+          package = dwWithAcronyms;
+          usersFile = "/var/lib/dokuwiki/site2.local/users.auth.php";
+          plugins = [ plugin-icalevents ];
+          settings = {
+            useacl = true;
+            superuser = "admin";
+            title._file = titleFile;
+            plugin.dummy.empty = "This is just for testing purposes";
+          };
+          acl = [
+            { page = "*";
+              actor = "@ALL";
+              level = "read"; }
+            { page = "acl-test";
+              actor = "@ALL";
+              level = "none"; }
+          ];
+          pluginsConfig = {
+            authad = false;
+            authldap = false;
+            authmysql = false;
+            authpgsql = false;
+            tag = false;
+            icalevents = true;
+          };
+        };
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = [ 80 ];
+    networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
+  };
+
+  titleFile = pkgs.writeText "dokuwiki-title" "DokuWiki on site2";
 in {
   name = "dokuwiki";
   meta = with pkgs.lib; {
     maintainers = with maintainers; [
       _1000101
       onny
+      e1mo
     ];
   };
 
   nodes = {
-    dokuwiki_nginx = {...}: {
-      services.dokuwiki = {
-        sites = {
-          "site1.local" = {
-            aclUse = false;
-            superUser = "admin";
-          };
-          "site2.local" = {
-            usersFile = "/var/lib/dokuwiki/site2.local/users.auth.php";
-            superUser = "admin";
-            templates = [ template-bootstrap3 ];
-            plugins = [ plugin-icalevents ];
-          };
-        };
-      };
-
-      networking.firewall.allowedTCPPorts = [ 80 ];
-      networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
-    };
-
-    dokuwiki_caddy = {...}: {
-      services.dokuwiki = {
-        webserver = "caddy";
-        sites = {
-          "site1.local" = {
-            aclUse = false;
-            superUser = "admin";
-          };
-          "site2.local" = {
-            usersFile = "/var/lib/dokuwiki/site2.local/users.auth.php";
-            superUser = "admin";
-            templates = [ template-bootstrap3 ];
-            plugins = [ plugin-icalevents ];
-          };
-        };
-      };
-
-      networking.firewall.allowedTCPPorts = [ 80 ];
-      networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
-    };
-
+    dokuwiki_nginx = mkNode "nginx";
+    dokuwiki_caddy = mkNode "caddy";
   };
 
   testScript = ''
@@ -99,13 +114,48 @@ in {
         machine.succeed("curl -sSfL http://site1.local/ | grep 'DokuWiki'")
         machine.fail("curl -sSfL 'http://site1.local/doku.php?do=login' | grep 'Login'")
 
-        machine.succeed("curl -sSfL http://site2.local/ | grep 'DokuWiki'")
+        machine.succeed("curl -sSfL http://site2.local/ | grep 'DokuWiki on site2'")
         machine.succeed("curl -sSfL 'http://site2.local/doku.php?do=login' | grep 'Login'")
 
-        machine.succeed(
+        with subtest("ACL Operations"):
+          machine.succeed(
             "echo 'admin:$2y$10$ijdBQMzSVV20SrKtCna8gue36vnsbVm2wItAXvdm876sshI4uwy6S:Admin:admin@example.test:user' >> /var/lib/dokuwiki/site2.local/users.auth.php",
             "curl -sSfL -d 'u=admin&p=password' --cookie-jar cjar 'http://site2.local/doku.php?do=login'",
             "curl -sSfL --cookie cjar --cookie-jar cjar 'http://site2.local/doku.php?do=login' | grep 'Logged in as: <bdi>Admin</bdi>'",
-        )
+          )
+
+          # Ensure the generated ACL is valid
+          machine.succeed(
+            "echo 'No Hello World! for @ALL here' >> /var/lib/dokuwiki/site2.local/data/pages/acl-test.txt",
+            "curl -sSL 'http://site2.local/doku.php?id=acl-test' | grep 'Permission Denied'"
+          )
+
+        with subtest("Customizing Dokuwiki"):
+          machine.succeed(
+            "echo 'r13y is awesome!' >> /var/lib/dokuwiki/site2.local/data/pages/acronyms-test.txt",
+            "curl -sSfL 'http://site2.local/doku.php?id=acronyms-test' | grep '<abbr title=\"reproducibility\">r13y</abbr>'",
+          )
+
+          # Testing if plugins (a) be correctly loaded and (b) configuration to enable them works
+          machine.succeed(
+              "echo '~~INFO:syntaxplugins~~' >> /var/lib/dokuwiki/site2.local/data/pages/plugin-list.txt",
+              "curl -sSfL 'http://site2.local/doku.php?id=plugin-list' | grep 'plugin:icalevents'",
+              "curl -sSfL 'http://site2.local/doku.php?id=plugin-list' | (! grep 'plugin:tag')",
+          )
+
+          # Test if theme is applied and working correctly (no weired relative PHP import errors)
+          machine.succeed(
+            "curl -sSfL 'http://site1.local/doku.php' | grep 'bootstrap3/images/logo.png'",
+            "curl -sSfL 'http://site1.local/lib/exe/css.php' | grep 'bootstrap3'",
+            "curl -sSfL 'http://site1.local/lib/tpl/bootstrap3/css.php'",
+          )
+
+
+        # Just to ensure both Webserver configurations are consistent in allowing that
+        with subtest("Rewriting"):
+          machine.succeed(
+            "echo 'Hello, NixOS!' >> /var/lib/dokuwiki/site1.local/data/pages/rewrite-test.txt",
+            "curl -sSfL http://site1.local/rewrite-test | grep 'Hello, NixOS!'",
+          )
   '';
 })
diff --git a/nixos/tests/early-mount-options.nix b/nixos/tests/early-mount-options.nix
new file mode 100644
index 00000000000..8be318ae13b
--- /dev/null
+++ b/nixos/tests/early-mount-options.nix
@@ -0,0 +1,19 @@
+# Test for https://github.com/NixOS/nixpkgs/pull/193469
+import ./make-test-python.nix {
+  name = "early-mount-options";
+
+  nodes.machine = {
+    virtualisation.fileSystems."/var" = {
+      options = [ "bind" "nosuid" "nodev" "noexec" ];
+      device = "/var";
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+
+    var_mount_info = machine.succeed("findmnt /var -n -o OPTIONS")
+    options = var_mount_info.strip().split(",")
+    assert "nosuid" in options and "nodev" in options and "noexec" in options
+  '';
+}
diff --git a/nixos/tests/elk.nix b/nixos/tests/elk.nix
index f42be00f23b..5c332cb5f2e 100644
--- a/nixos/tests/elk.nix
+++ b/nixos/tests/elk.nix
@@ -268,14 +268,6 @@ let
     '';
   }) { inherit pkgs system; };
 in {
-  ELK-6 = mkElkTest "elk-6-oss" {
-    name = "elk-6-oss";
-    elasticsearch = pkgs.elasticsearch6-oss;
-    logstash      = pkgs.logstash6-oss;
-    kibana        = pkgs.kibana6-oss;
-    journalbeat   = pkgs.journalbeat6;
-    metricbeat    = pkgs.metricbeat6;
-  };
   # We currently only package upstream binaries.
   # Feel free to package an SSPL licensed source-based package!
   # ELK-7 = mkElkTest "elk-7-oss" {
@@ -287,13 +279,6 @@ in {
   #   metricbeat    = pkgs.metricbeat7;
   # };
   unfree = lib.dontRecurseIntoAttrs {
-    ELK-6 = mkElkTest "elk-6" {
-      elasticsearch = pkgs.elasticsearch6;
-      logstash      = pkgs.logstash6;
-      kibana        = pkgs.kibana6;
-      journalbeat   = pkgs.journalbeat6;
-      metricbeat    = pkgs.metricbeat6;
-    };
     ELK-7 = mkElkTest "elk-7" {
       elasticsearch = pkgs.elasticsearch7;
       logstash      = pkgs.logstash7;
diff --git a/nixos/tests/envfs.nix b/nixos/tests/envfs.nix
new file mode 100644
index 00000000000..3f9cd1edb59
--- /dev/null
+++ b/nixos/tests/envfs.nix
@@ -0,0 +1,42 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+let
+  pythonShebang = pkgs.writeScript "python-shebang" ''
+    #!/usr/bin/python
+    print("OK")
+  '';
+
+  bashShebang = pkgs.writeScript "bash-shebang" ''
+    #!/usr/bin/bash
+    echo "OK"
+  '';
+in
+{
+  name = "envfs";
+  nodes.machine.services.envfs.enable = true;
+
+  testScript = ''
+    start_all()
+    machine.wait_until_succeeds("mountpoint -q /usr/bin/")
+    machine.succeed(
+        "PATH=${pkgs.coreutils}/bin /usr/bin/cp --version",
+        # check fallback paths
+        "PATH= /usr/bin/sh --version",
+        "PATH= /usr/bin/env --version",
+        "PATH= test -e /usr/bin/sh",
+        "PATH= test -e /usr/bin/env",
+        # no stat
+        "! test -e /usr/bin/cp",
+        # also picks up PATH that was set after execve
+        "! /usr/bin/hello",
+        "PATH=${pkgs.hello}/bin /usr/bin/hello",
+    )
+
+    out = machine.succeed("PATH=${pkgs.python3}/bin ${pythonShebang}")
+    print(out)
+    assert out == "OK\n"
+
+    out = machine.succeed("PATH=${pkgs.bash}/bin ${bashShebang}")
+    print(out)
+    assert out == "OK\n"
+  '';
+})
diff --git a/nixos/tests/envoy.nix b/nixos/tests/envoy.nix
index 9d2c32ce102..1e4bfe62639 100644
--- a/nixos/tests/envoy.nix
+++ b/nixos/tests/envoy.nix
@@ -13,7 +13,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
           socket_address = {
             protocol = "TCP";
             address = "127.0.0.1";
-            port_value = 9901;
+            port_value = 80;
           };
         };
       };
@@ -22,12 +22,33 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
         clusters = [];
       };
     };
+    specialisation = {
+      withoutConfigValidation.configuration = { ... }: {
+        services.envoy = {
+          requireValidConfig = false;
+          settings.admin.access_log_path = lib.mkForce "/var/log/envoy/access.log";
+        };
+      };
+    };
   };
 
-  testScript = ''
-    machine.start()
-    machine.wait_for_unit("envoy.service")
-    machine.wait_for_open_port(9901)
-    machine.wait_until_succeeds("curl -fsS localhost:9901/ready")
-  '';
+  testScript = { nodes, ... }:
+    let
+      specialisations = "${nodes.machine.system.build.toplevel}/specialisation";
+    in
+    ''
+      machine.start()
+
+      with subtest("envoy.service starts and responds with ready"):
+        machine.wait_for_unit("envoy.service")
+        machine.wait_for_open_port(80)
+        machine.wait_until_succeeds("curl -fsS localhost:80/ready")
+
+      with subtest("envoy.service works with config path not available at eval time"):
+        machine.succeed('${specialisations}/withoutConfigValidation/bin/switch-to-configuration test')
+        machine.wait_for_unit("envoy.service")
+        machine.wait_for_open_port(80)
+        machine.wait_until_succeeds("curl -fsS localhost:80/ready")
+        machine.succeed('test -f /var/log/envoy/access.log')
+    '';
 })
diff --git a/nixos/tests/esphome.nix b/nixos/tests/esphome.nix
new file mode 100644
index 00000000000..b8dbdb0b379
--- /dev/null
+++ b/nixos/tests/esphome.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  testPort = 6052;
+  unixSocket = "/run/esphome/esphome.sock";
+in
+with lib;
+{
+  name = "esphome";
+  meta.maintainers = with pkgs.lib.maintainers; [ oddlama ];
+
+  nodes = {
+    esphomeTcp = { ... }:
+      {
+        services.esphome = {
+          enable = true;
+          port = testPort;
+          address = "0.0.0.0";
+          openFirewall = true;
+        };
+      };
+
+    esphomeUnix = { ... }:
+      {
+        services.esphome = {
+          enable = true;
+          enableUnixSocket = true;
+        };
+      };
+  };
+
+  testScript = ''
+    esphomeTcp.wait_for_unit("esphome.service")
+    esphomeTcp.wait_for_open_port(${toString testPort})
+    esphomeTcp.succeed("curl --fail http://localhost:${toString testPort}/")
+
+    esphomeUnix.wait_for_unit("esphome.service")
+    esphomeUnix.wait_for_file("${unixSocket}")
+    esphomeUnix.succeed("curl --fail --unix-socket ${unixSocket} http://localhost/")
+  '';
+})
diff --git a/nixos/tests/evcc.nix b/nixos/tests/evcc.nix
index c223977a9d8..b445735ede9 100644
--- a/nixos/tests/evcc.nix
+++ b/nixos/tests/evcc.nix
@@ -88,7 +88,6 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
     with subtest("Check journal for errors"):
         _, output = machine.execute("journalctl -o cat -u evcc.service")
         assert "FATAL" not in output
-        assert "ERROR" not in output
 
     with subtest("Check systemd hardening"):
         _, output = machine.execute("systemd-analyze security evcc.service | grep -v '✓'")
diff --git a/nixos/tests/fcitx/config b/nixos/tests/fcitx/config
deleted file mode 100644
index 169768994e2..00000000000
--- a/nixos/tests/fcitx/config
+++ /dev/null
@@ -1,12 +0,0 @@
-[Hotkey]
-SwitchKey=Disabled
-IMSwitchHotkey=ALT_SHIFT
-TimeInterval=240
-
-[Program]
-DelayStart=5
-
-[Output]
-
-[Appearance]
-
diff --git a/nixos/tests/fcitx/profile b/nixos/tests/fcitx/profile
deleted file mode 100644
index 77497a1496b..00000000000
--- a/nixos/tests/fcitx/profile
+++ /dev/null
@@ -1,4 +0,0 @@
-[Profile]
-IMName=zhengma-large
-EnabledIMList=fcitx-keyboard-us:True,zhengma-large:True,m17n_sa_harvard-kyoto:True
-PreeditStringInClientWindow=False
diff --git a/nixos/tests/fcitx5/config b/nixos/tests/fcitx5/config
new file mode 100644
index 00000000000..cf4334639f1
--- /dev/null
+++ b/nixos/tests/fcitx5/config
@@ -0,0 +1,11 @@
+[Hotkey]
+EnumerateSkipFirst=False
+
+[Hotkey/TriggerKeys]
+0=Control+space
+
+[Hotkey/EnumerateForwardKeys]
+0=Alt+Shift_L
+
+[Hotkey/EnumerateBackwardKeys]
+0=Alt+Shift_R
diff --git a/nixos/tests/fcitx/default.nix b/nixos/tests/fcitx5/default.nix
index c132249fcb2..261a5f1f45c 100644
--- a/nixos/tests/fcitx/default.nix
+++ b/nixos/tests/fcitx5/default.nix
@@ -1,64 +1,48 @@
-import ../make-test-python.nix (
+import ../make-test-python.nix ({ pkgs, ... }:
+# copy_from_host works only for store paths
+rec {
+  name = "fcitx5";
+  nodes.machine = { pkgs, ... }:
   {
-    pkgs, ...
-  }:
-    # copy_from_host works only for store paths
-    rec {
-        name = "fcitx";
-        meta.broken = true; # takes hours to time out since October 2021
-        nodes.machine =
-        {
-          pkgs,
-          ...
-        }:
-          {
-
-            imports = [
-              ../common/user-account.nix
-            ];
-
-            environment.systemPackages = [
-              # To avoid clashing with xfce4-terminal
-              pkgs.alacritty
-            ];
-
-
-            services.xserver =
-            {
-              enable = true;
-
-              displayManager = {
-                lightdm.enable = true;
-                autoLogin = {
-                  enable = true;
-                  user = "alice";
-                };
-              };
-
-              desktopManager.xfce.enable = true;
-            };
-
-            i18n = {
-              inputMethod = {
-                enabled = "fcitx";
-                fcitx.engines = [
-                  pkgs.fcitx-engines.m17n
-                  pkgs.fcitx-engines.table-extra
-                ];
-              };
-            };
-          }
-        ;
-
-        testScript = { nodes, ... }:
-        let
-            user = nodes.machine.config.users.users.alice;
-            userName      = user.name;
-            userHome      = user.home;
-            xauth         = "${userHome}/.Xauthority";
-            fcitx_confdir = "${userHome}/.config/fcitx";
-        in
-        ''
+    imports = [
+      ../common/user-account.nix
+    ];
+
+    environment.systemPackages = [
+      # To avoid clashing with xfce4-terminal
+      pkgs.alacritty
+    ];
+
+    services.xserver = {
+      enable = true;
+
+      displayManager = {
+        lightdm.enable = true;
+        autoLogin = {
+          enable = true;
+          user = "alice";
+        };
+      };
+
+      desktopManager.xfce.enable = true;
+    };
+
+    i18n.inputMethod = {
+      enabled = "fcitx5";
+      fcitx5.addons = [
+        pkgs.fcitx5-m17n
+        pkgs.fcitx5-chinese-addons
+      ];
+    };
+  };
+
+  testScript = { nodes, ... }:
+    let
+      user = nodes.machine.users.users.alice;
+      xauth         = "${user.home}/.Xauthority";
+      fcitx_confdir = "${user.home}/.config/fcitx5";
+    in
+      ''
             # We need config files before login session
             # So copy first thing
 
@@ -75,13 +59,13 @@ import ../make-test-python.nix (
 
             start_all()
 
-            machine.wait_for_file("${xauth}")
+            machine.wait_for_file("${xauth}}")
             machine.succeed("xauth merge ${xauth}")
 
             machine.sleep(5)
 
-            machine.succeed("su - ${userName} -c 'alacritty&'")
-            machine.succeed("su - ${userName} -c 'fcitx&'")
+            machine.succeed("su - ${user.name} -c 'alacritty&'")
+            machine.succeed("su - ${user.name} -c 'fcitx5&'")
             machine.sleep(10)
 
             ### Type on terminal
@@ -109,8 +93,10 @@ import ../make-test-python.nix (
             machine.send_key("ctrl-spc")
             machine.sleep(1)
 
-            ### Default zhengma, enter 一下
-            machine.send_chars("a2")
+            ### Default wubi, enter 一下
+            machine.send_chars("gggh")
+            machine.sleep(1)
+            machine.send_key("\n")
             machine.sleep(1)
 
             ### Switch to Harvard Kyoto
@@ -134,9 +120,8 @@ import ../make-test-python.nix (
             machine.screenshot("terminal_chars")
 
             ### Verify that file contents are as expected
-            file_content = machine.succeed("cat ${userHome}/fcitx_test.out")
+            file_content = machine.succeed("cat ${user.home}/fcitx_test.out")
             assert file_content == "☺一下क\n"
             ''
-    ;
-  }
-)
+  ;
+})
diff --git a/nixos/tests/fcitx5/profile b/nixos/tests/fcitx5/profile
new file mode 100644
index 00000000000..55e7b7b459f
--- /dev/null
+++ b/nixos/tests/fcitx5/profile
@@ -0,0 +1,15 @@
+[Groups/0]
+Name=NixOS_test
+Default Layout=us
+DefaultIM=wbx
+
+[Groups/0/Items/0]
+Name=wbx
+Layout=us
+
+[Groups/0/Items/1]
+Name=m17n_sa_harvard-kyoto
+Layout=us
+
+[GroupOrder]
+0=NixOS_test
diff --git a/nixos/tests/firewall.nix b/nixos/tests/firewall.nix
index 5c434c1cb6d..dd7551f143a 100644
--- a/nixos/tests/firewall.nix
+++ b/nixos/tests/firewall.nix
@@ -1,7 +1,7 @@
 # Test the firewall module.
 
-import ./make-test-python.nix ( { pkgs, ... } : {
-  name = "firewall";
+import ./make-test-python.nix ( { pkgs, nftables, ... } : {
+  name = "firewall" + pkgs.lib.optionalString nftables "-nftables";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ eelco ];
   };
@@ -11,6 +11,7 @@ import ./make-test-python.nix ( { pkgs, ... } : {
         { ... }:
         { networking.firewall.enable = true;
           networking.firewall.logRefusedPackets = true;
+          networking.nftables.enable = nftables;
           services.httpd.enable = true;
           services.httpd.adminAddr = "foo@example.org";
         };
@@ -23,6 +24,7 @@ import ./make-test-python.nix ( { pkgs, ... } : {
         { ... }:
         { networking.firewall.enable = true;
           networking.firewall.rejectPackets = true;
+          networking.nftables.enable = nftables;
         };
 
       attacker =
@@ -35,10 +37,11 @@ import ./make-test-python.nix ( { pkgs, ... } : {
 
   testScript = { nodes, ... }: let
     newSystem = nodes.walled2.config.system.build.toplevel;
+    unit = if nftables then "nftables" else "firewall";
   in ''
     start_all()
 
-    walled.wait_for_unit("firewall")
+    walled.wait_for_unit("${unit}")
     walled.wait_for_unit("httpd")
     attacker.wait_for_unit("network.target")
 
@@ -54,12 +57,12 @@ import ./make-test-python.nix ( { pkgs, ... } : {
     walled.succeed("ping -c 1 attacker >&2")
 
     # If we stop the firewall, then connections should succeed.
-    walled.stop_job("firewall")
+    walled.stop_job("${unit}")
     attacker.succeed("curl -v http://walled/ >&2")
 
     # Check whether activation of a new configuration reloads the firewall.
     walled.succeed(
-        "${newSystem}/bin/switch-to-configuration test 2>&1 | grep -qF firewall.service"
+        "${newSystem}/bin/switch-to-configuration test 2>&1 | grep -qF ${unit}.service"
     )
   '';
 })
diff --git a/nixos/tests/freshrss-pgsql.nix b/nixos/tests/freshrss-pgsql.nix
new file mode 100644
index 00000000000..055bd51ed43
--- /dev/null
+++ b/nixos/tests/freshrss-pgsql.nix
@@ -0,0 +1,48 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "freshrss";
+  meta.maintainers = with lib.maintainers; [ etu stunkymonkey ];
+
+  nodes.machine = { pkgs, ... }: {
+    services.freshrss = {
+      enable = true;
+      baseUrl = "http://localhost";
+      passwordFile = pkgs.writeText "password" "secret";
+      dataDir = "/srv/freshrss";
+      database = {
+        type = "pgsql";
+        port = 5432;
+        user = "freshrss";
+        passFile = pkgs.writeText "db-password" "db-secret";
+      };
+    };
+
+    services.postgresql = {
+      enable = true;
+      ensureDatabases = [ "freshrss" ];
+      ensureUsers = [
+        {
+          name = "freshrss";
+          ensurePermissions = {
+            "DATABASE freshrss" = "ALL PRIVILEGES";
+          };
+        }
+      ];
+      initialScript = pkgs.writeText "postgresql-password" ''
+        CREATE ROLE freshrss WITH LOGIN PASSWORD 'db-secret' CREATEDB;
+      '';
+    };
+
+    systemd.services."freshrss-config" = {
+      requires = [ "postgresql.service" ];
+      after = [ "postgresql.service" ];
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_open_port(5432)
+    machine.wait_for_open_port(80)
+    response = machine.succeed("curl -vvv -s -H 'Host: freshrss' http://127.0.0.1:80/i/")
+    assert '<title>Login · FreshRSS</title>' in response, "Login page didn't load successfully"
+  '';
+})
diff --git a/nixos/tests/freshrss.nix b/nixos/tests/freshrss-sqlite.nix
index 7bdbf29e923..b821c98a7e7 100644
--- a/nixos/tests/freshrss.nix
+++ b/nixos/tests/freshrss-sqlite.nix
@@ -7,6 +7,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
       enable = true;
       baseUrl = "http://localhost";
       passwordFile = pkgs.writeText "password" "secret";
+      dataDir = "/srv/freshrss";
     };
   };
 
diff --git a/nixos/tests/fsck.nix b/nixos/tests/fsck.nix
index 5b8b09f433a..ccb664be080 100644
--- a/nixos/tests/fsck.nix
+++ b/nixos/tests/fsck.nix
@@ -1,3 +1,9 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+, systemdStage1 ? false
+}:
+
 import ./make-test-python.nix {
   name = "fsck";
 
@@ -11,13 +17,17 @@ import ./make-test-python.nix {
         autoFormat = true;
       };
     };
+
+    boot.initrd.systemd.enable = systemdStage1;
   };
 
   testScript = ''
     machine.wait_for_unit("default.target")
 
     with subtest("root fs is fsckd"):
-        machine.succeed("journalctl -b | grep 'fsck.ext4.*/dev/vda'")
+        machine.succeed("journalctl -b | grep '${if systemdStage1
+          then "fsck.*vda.*clean"
+          else "fsck.ext4.*/dev/vda"}'")
 
     with subtest("mnt fs is fsckd"):
         machine.succeed("journalctl -b | grep 'fsck.*/dev/vdb.*clean'")
diff --git a/nixos/tests/ft2-clone.nix b/nixos/tests/ft2-clone.nix
index 3c90b3d3fa2..a8395d4ebaa 100644
--- a/nixos/tests/ft2-clone.nix
+++ b/nixos/tests/ft2-clone.nix
@@ -26,9 +26,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
       machine.wait_for_window(r"Fasttracker")
       machine.sleep(5)
-      # One of the few words that actually get recognized
-      if "Songlen" not in machine.get_screen_text():
-          raise Exception("Program did not start successfully")
+      machine.wait_for_text(r"(Songlen|Repstart|Time|About|Nibbles|Help)")
       machine.screenshot("screen")
     '';
 })
diff --git a/nixos/tests/garage/basic.nix b/nixos/tests/garage/basic.nix
new file mode 100644
index 00000000000..b6df1e72af9
--- /dev/null
+++ b/nixos/tests/garage/basic.nix
@@ -0,0 +1,98 @@
+args@{ mkNode, ... }:
+(import ../make-test-python.nix ({ pkgs, ...} : {
+  name = "garage-basic";
+  meta = {
+    maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
+  };
+
+  nodes = {
+    single_node = mkNode { replicationMode = "none"; };
+  };
+
+  testScript = ''
+    from typing import List
+    from dataclasses import dataclass
+    import re
+
+    start_all()
+
+    cur_version_regex = re.compile('Current cluster layout version: (?P<ver>\d*)')
+    key_creation_regex = re.compile('Key name: (?P<key_name>.*)\nKey ID: (?P<key_id>.*)\nSecret key: (?P<secret_key>.*)')
+
+    @dataclass
+    class S3Key:
+       key_name: str
+       key_id: str
+       secret_key: str
+
+    @dataclass
+    class GarageNode:
+       node_id: str
+       host: str
+
+    def get_node_fqn(machine: Machine) -> GarageNode:
+      node_id, host = machine.succeed("garage node id").split('@')
+      return GarageNode(node_id=node_id, host=host)
+
+    def get_node_id(machine: Machine) -> str:
+      return get_node_fqn(machine).node_id
+
+    def get_layout_version(machine: Machine) -> int:
+      version_data = machine.succeed("garage layout show")
+      m = cur_version_regex.search(version_data)
+      if m and m.group('ver') is not None:
+        return int(m.group('ver')) + 1
+      else:
+        raise ValueError('Cannot find current layout version')
+
+    def apply_garage_layout(machine: Machine, layouts: List[str]):
+       for layout in layouts:
+          machine.succeed(f"garage layout assign {layout}")
+       version = get_layout_version(machine)
+       machine.succeed(f"garage layout apply --version {version}")
+
+    def create_api_key(machine: Machine, key_name: str) -> S3Key:
+       output = machine.succeed(f"garage key new --name {key_name}")
+       m = key_creation_regex.match(output)
+       if not m or not m.group('key_id') or not m.group('secret_key'):
+          raise ValueError('Cannot parse API key data')
+       return S3Key(key_name=key_name, key_id=m.group('key_id'), secret_key=m.group('secret_key'))
+
+    def get_api_key(machine: Machine, key_pattern: str) -> S3Key:
+       output = machine.succeed(f"garage key info {key_pattern}")
+       m = key_creation_regex.match(output)
+       if not m or not m.group('key_name') or not m.group('key_id') or not m.group('secret_key'):
+           raise ValueError('Cannot parse API key data')
+       return S3Key(key_name=m.group('key_name'), key_id=m.group('key_id'), secret_key=m.group('secret_key'))
+
+    def test_bucket_writes(node):
+      node.succeed("garage bucket create test-bucket")
+      s3_key = create_api_key(node, "test-api-key")
+      node.succeed("garage bucket allow --read --write test-bucket --key test-api-key")
+      other_s3_key = get_api_key(node, 'test-api-key')
+      assert other_s3_key.secret_key == other_s3_key.secret_key
+      node.succeed(
+        f"mc alias set test-garage http://[::1]:3900 {s3_key.key_id} {s3_key.secret_key} --api S3v4"
+      )
+      node.succeed("echo test | mc pipe test-garage/test-bucket/test.txt")
+      assert node.succeed("mc cat test-garage/test-bucket/test.txt").strip() == "test"
+
+    def test_bucket_over_http(node, bucket='test-bucket', url=None):
+      if url is None:
+         url = f"{bucket}.web.garage"
+
+      node.succeed(f'garage bucket website --allow {bucket}')
+      node.succeed(f'echo hello world | mc pipe test-garage/{bucket}/index.html')
+      assert (node.succeed(f"curl -H 'Host: {url}' http://localhost:3902")).strip() == 'hello world'
+
+    with subtest("Garage works as a single-node S3 storage"):
+      single_node.wait_for_unit("garage.service")
+      single_node.wait_for_open_port(3900)
+      # Now Garage is initialized.
+      single_node_id = get_node_id(single_node)
+      apply_garage_layout(single_node, [f'-z qemutest -c 1 "{single_node_id}"'])
+      # Now Garage is operational.
+      test_bucket_writes(single_node)
+      test_bucket_over_http(single_node)
+  '';
+})) args
diff --git a/nixos/tests/garage/default.nix b/nixos/tests/garage/default.nix
new file mode 100644
index 00000000000..0a1ccde056b
--- /dev/null
+++ b/nixos/tests/garage/default.nix
@@ -0,0 +1,53 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../../.. { inherit system config; }
+}:
+with pkgs.lib;
+
+let
+    mkNode = package: { replicationMode, publicV6Address ? "::1" }: { pkgs, ... }: {
+      networking.interfaces.eth1.ipv6.addresses = [{
+        address = publicV6Address;
+        prefixLength = 64;
+      }];
+
+      networking.firewall.allowedTCPPorts = [ 3901 3902 ];
+
+      services.garage = {
+        enable = true;
+        inherit package;
+        settings = {
+          replication_mode = replicationMode;
+
+          rpc_bind_addr = "[::]:3901";
+          rpc_public_addr = "[${publicV6Address}]:3901";
+          rpc_secret = "5c1915fa04d0b6739675c61bf5907eb0fe3d9c69850c83820f51b4d25d13868c";
+
+          s3_api = {
+            s3_region = "garage";
+            api_bind_addr = "[::]:3900";
+            root_domain = ".s3.garage";
+          };
+
+          s3_web = {
+            bind_addr = "[::]:3902";
+            root_domain = ".web.garage";
+            index = "index.html";
+          };
+        };
+      };
+      environment.systemPackages = [ pkgs.minio-client ];
+
+      # Garage requires at least 1GiB of free disk space to run.
+      virtualisation.diskSize = 2 * 1024;
+    };
+in
+  foldl
+  (matrix: ver: matrix // {
+    "basic${toString ver}" = import ./basic.nix { inherit system pkgs; mkNode = mkNode pkgs."garage_${ver}"; };
+    "with-3node-replication${toString ver}" = import ./with-3node-replication.nix { inherit system pkgs; mkNode = mkNode pkgs."garage_${ver}"; };
+  })
+  {}
+  [
+    "0_8"
+  ]
diff --git a/nixos/tests/garage.nix b/nixos/tests/garage/with-3node-replication.nix
index dc1f83e7f8f..d372ad1aa00 100644
--- a/nixos/tests/garage.nix
+++ b/nixos/tests/garage/with-3node-replication.nix
@@ -1,50 +1,12 @@
-import ./make-test-python.nix ({ pkgs, ...} :
-let
-    mkNode = { replicationMode, publicV6Address ? "::1" }: { pkgs, ... }: {
-      networking.interfaces.eth1.ipv6.addresses = [{
-        address = publicV6Address;
-        prefixLength = 64;
-      }];
-
-      networking.firewall.allowedTCPPorts = [ 3901 3902 ];
-
-      services.garage = {
-        enable = true;
-        settings = {
-          replication_mode = replicationMode;
-
-          rpc_bind_addr = "[::]:3901";
-          rpc_public_addr = "[${publicV6Address}]:3901";
-          rpc_secret = "5c1915fa04d0b6739675c61bf5907eb0fe3d9c69850c83820f51b4d25d13868c";
-
-          s3_api = {
-            s3_region = "garage";
-            api_bind_addr = "[::]:3900";
-            root_domain = ".s3.garage";
-          };
-
-          s3_web = {
-            bind_addr = "[::]:3902";
-            root_domain = ".web.garage";
-            index = "index.html";
-          };
-        };
-      };
-      environment.systemPackages = [ pkgs.minio-client ];
-
-      # Garage requires at least 1GiB of free disk space to run.
-      virtualisation.diskSize = 2 * 1024;
-    };
-
-
-in {
-  name = "garage";
+args@{ mkNode, ... }:
+(import ../make-test-python.nix ({ pkgs, ...} :
+{
+  name = "garage-3node-replication";
   meta = {
     maintainers = with pkgs.lib.maintainers; [ raitobezarius ];
   };
 
   nodes = {
-    single_node = mkNode { replicationMode = "none"; };
     node1 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::1"; };
     node2 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::2"; };
     node3 = mkNode { replicationMode = 3; publicV6Address = "fc00:1::3"; };
@@ -126,16 +88,6 @@ in {
       node.succeed(f'echo hello world | mc pipe test-garage/{bucket}/index.html')
       assert (node.succeed(f"curl -H 'Host: {url}' http://localhost:3902")).strip() == 'hello world'
 
-    with subtest("Garage works as a single-node S3 storage"):
-      single_node.wait_for_unit("garage.service")
-      single_node.wait_for_open_port(3900)
-      # Now Garage is initialized.
-      single_node_id = get_node_id(single_node)
-      apply_garage_layout(single_node, [f'-z qemutest -c 1 "{single_node_id}"'])
-      # Now Garage is operational.
-      test_bucket_writes(single_node)
-      test_bucket_over_http(single_node)
-
     with subtest("Garage works as a multi-node S3 storage"):
       nodes = ('node1', 'node2', 'node3', 'node4')
       rev_machines = {m.name: m for m in machines}
@@ -166,4 +118,4 @@ in {
       for node in nodes:
          test_bucket_over_http(get_machine(node))
   '';
-})
+})) args
diff --git a/nixos/tests/gemstash.nix b/nixos/tests/gemstash.nix
new file mode 100644
index 00000000000..bc152e42e92
--- /dev/null
+++ b/nixos/tests/gemstash.nix
@@ -0,0 +1,51 @@
+{ system ? builtins.currentSystem, config ? { }
+, pkgs ? import ../.. { inherit system config; } }:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let common_meta = { maintainers = [ maintainers.viraptor ]; };
+in
+{
+  gemstash_works = makeTest {
+    name = "gemstash-works";
+    meta = common_meta;
+
+    nodes.machine = { config, pkgs, ... }: {
+      services.gemstash = {
+        enable = true;
+      };
+    };
+
+    # gemstash responds to http requests
+    testScript = ''
+      machine.wait_for_unit("gemstash.service")
+      machine.wait_for_file("/var/lib/gemstash")
+      machine.wait_for_open_port(9292)
+      machine.succeed("curl http://localhost:9292")
+    '';
+  };
+
+  gemstash_custom_port = makeTest {
+    name = "gemstash-custom-port";
+    meta = common_meta;
+
+    nodes.machine = { config, pkgs, ... }: {
+      services.gemstash = {
+        enable = true;
+        openFirewall = true;
+        settings = {
+          bind = "tcp://0.0.0.0:12345";
+        };
+      };
+    };
+
+    # gemstash responds to http requests
+    testScript = ''
+      machine.wait_for_unit("gemstash.service")
+      machine.wait_for_file("/var/lib/gemstash")
+      machine.wait_for_open_port(12345)
+      machine.succeed("curl http://localhost:12345")
+    '';
+  };
+}
diff --git a/nixos/tests/gitea.nix b/nixos/tests/gitea.nix
index 68a2566c119..c38aad1f44e 100644
--- a/nixos/tests/gitea.nix
+++ b/nixos/tests/gitea.nix
@@ -1,5 +1,6 @@
 { system ? builtins.currentSystem,
   config ? {},
+  giteaPackage ? pkgs.gitea,
   pkgs ? import ../.. { inherit system config; }
 }:
 
@@ -7,10 +8,25 @@ with import ../lib/testing-python.nix { inherit system pkgs; };
 with pkgs.lib;
 
 let
+  ## gpg --faked-system-time='20230301T010000!' --quick-generate-key snakeoil ed25519 sign
+  signingPrivateKey = ''
+    -----BEGIN PGP PRIVATE KEY BLOCK-----
+
+    lFgEY/6jkBYJKwYBBAHaRw8BAQdADXiZRV8RJUyC9g0LH04wLMaJL9WTc+szbMi7
+    5fw4yP8AAQCl8EwGfzSLm/P6fCBfA3I9znFb3MEHGCCJhJ6VtKYyRw7ktAhzbmFr
+    ZW9pbIiUBBMWCgA8FiEE+wUM6VW/NLtAdSixTWQt6LZ4x50FAmP+o5ACGwMFCQPC
+    ZwAECwkIBwQVCgkIBRYCAwEAAh4FAheAAAoJEE1kLei2eMedFTgBAKQs1oGFZrCI
+    TZP42hmBTKxGAI1wg7VSdDEWTZxut/2JAQDGgo2sa4VHMfj0aqYGxrIwfP2B7JHO
+    GCqGCRf9O/hzBA==
+    =9Uy3
+    -----END PGP PRIVATE KEY BLOCK-----
+  '';
+  signingPrivateKeyId = "4D642DE8B678C79D";
+
   supportedDbTypes = [ "mysql" "postgres" "sqlite3" ];
   makeGiteaTest = type: nameValuePair type (makeTest {
-    name = "gitea-${type}";
-    meta.maintainers = with maintainers; [ aanderse kolaente ma27 ];
+    name = "${giteaPackage.pname}-${type}";
+    meta.maintainers = with maintainers; [ aanderse indeednotjames kolaente ma27 ];
 
     nodes = {
       server = { config, pkgs, ... }: {
@@ -18,9 +34,11 @@ let
         services.gitea = {
           enable = true;
           database = { inherit type; };
+          package = giteaPackage;
           settings.service.DISABLE_REGISTRATION = true;
+          settings."repository.signing".SIGNING_KEY = signingPrivateKeyId;
         };
-        environment.systemPackages = [ pkgs.gitea pkgs.jq ];
+        environment.systemPackages = [ giteaPackage pkgs.gnupg pkgs.jq ];
         services.openssh.enable = true;
       };
       client1 = { config, pkgs, ... }: {
@@ -54,9 +72,17 @@ let
 
       server.wait_for_unit("gitea.service")
       server.wait_for_open_port(3000)
+      server.wait_for_open_port(22)
       server.succeed("curl --fail http://localhost:3000/")
 
       server.succeed(
+          "su -l gitea -c 'gpg --homedir /var/lib/gitea/data/home/.gnupg "
+          + "--import ${toString (pkgs.writeText "gitea.key" signingPrivateKey)}'"
+      )
+
+      assert "BEGIN PGP PUBLIC KEY BLOCK" in server.succeed("curl http://localhost:3000/api/v1/signing-key.gpg")
+
+      server.succeed(
           "curl --fail http://localhost:3000/user/sign_up | grep 'Registration is disabled. "
           + "Please contact your site administrator.'"
       )
@@ -68,7 +94,7 @@ let
       api_token = server.succeed(
           "curl --fail -X POST http://test:totallysafe@localhost:3000/api/v1/users/test/tokens "
           + "-H 'Accept: application/json' -H 'Content-Type: application/json' -d "
-          + "'{\"name\":\"token\"}' | jq '.sha1' | xargs echo -n"
+          + "'{\"name\":\"token\",\"scopes\":[\"all\"]}' | jq '.sha1' | xargs echo -n"
       )
 
       server.succeed(
diff --git a/nixos/tests/github-runner.nix b/nixos/tests/github-runner.nix
new file mode 100644
index 00000000000..033365d6925
--- /dev/null
+++ b/nixos/tests/github-runner.nix
@@ -0,0 +1,37 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+{
+  name = "github-runner";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ veehaitch ];
+  };
+  nodes.machine = { pkgs, ... }: {
+    services.github-runners.test = {
+      enable = true;
+      url = "https://github.com/yaxitech";
+      tokenFile = builtins.toFile "github-runner.token" "not-so-secret";
+    };
+
+    systemd.services.dummy-github-com = {
+      wantedBy = [ "multi-user.target" ];
+      before = [ "github-runner-test.service" ];
+      script = "${pkgs.netcat}/bin/nc -Fl 443 | true && touch /tmp/registration-connect";
+    };
+    networking.hosts."127.0.0.1" = [ "api.github.com" ];
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("dummy-github-com")
+
+    try:
+      machine.wait_for_unit("github-runner-test")
+    except Exception:
+      pass
+
+    out = machine.succeed("journalctl -u github-runner-test")
+    assert "Self-hosted runner registration" in out, "did not read runner registration header"
+
+    machine.wait_until_succeeds("test -f /tmp/registration-connect")
+  '';
+})
diff --git a/nixos/tests/gitlab.nix b/nixos/tests/gitlab.nix
index d9d75d1cbd8..c2a11bada0a 100644
--- a/nixos/tests/gitlab.nix
+++ b/nixos/tests/gitlab.nix
@@ -17,13 +17,13 @@ let
 
   aliceUsername = "alice";
   aliceUserId = "2";
-  alicePassword = "alicepassword";
+  alicePassword = "R5twyCgU0uXC71wT9BBTCqLs6HFZ7h3L";
   aliceProjectId = "2";
   aliceProjectName = "test-alice";
 
   bobUsername = "bob";
   bobUserId = "3";
-  bobPassword = "bobpassword";
+  bobPassword = "XwkkBbl2SiIwabQzgcoaTbhsotijEEtF";
   bobProjectId = "3";
 in {
   name = "gitlab";
@@ -69,6 +69,10 @@ in {
         databasePasswordFile = pkgs.writeText "dbPassword" "xo0daiF4";
         initialRootPasswordFile = pkgs.writeText "rootPassword" initialRootPassword;
         smtp.enable = true;
+        pages = {
+          enable = true;
+          settings.pages-domain = "localhost";
+        };
         extraConfig = {
           incoming_email = {
             enabled = true;
@@ -79,11 +83,6 @@ in {
             host = "localhost";
             port = 143;
           };
-          # https://github.com/NixOS/nixpkgs/issues/132295
-          # pages = {
-          #   enabled = true;
-          #   host = "localhost";
-          # };
         };
         secrets = {
           secretFile = pkgs.writeText "secret" "Aig5zaic";
@@ -171,10 +170,9 @@ in {
       waitForServices = ''
         gitlab.wait_for_unit("gitaly.service")
         gitlab.wait_for_unit("gitlab-workhorse.service")
-        # https://github.com/NixOS/nixpkgs/issues/132295
-        # gitlab.wait_for_unit("gitlab-pages.service")
         gitlab.wait_for_unit("gitlab-mailroom.service")
         gitlab.wait_for_unit("gitlab.service")
+        gitlab.wait_for_unit("gitlab-pages.service")
         gitlab.wait_for_unit("gitlab-sidekiq.service")
         gitlab.wait_for_file("${nodes.gitlab.config.services.gitlab.statePath}/tmp/sockets/gitlab.socket")
         gitlab.wait_until_succeeds("curl -sSf http://gitlab/users/sign_in")
diff --git a/nixos/tests/gnome-flashback.nix b/nixos/tests/gnome-flashback.nix
new file mode 100644
index 00000000000..c97264e6928
--- /dev/null
+++ b/nixos/tests/gnome-flashback.nix
@@ -0,0 +1,51 @@
+import ./make-test-python.nix ({ pkgs, lib, ...} : {
+  name = "gnome-flashback";
+  meta = with lib; {
+    maintainers = teams.gnome.members ++ [ maintainers.chpatrick ];
+  };
+
+  nodes.machine = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+  in
+
+    { imports = [ ./common/user-account.nix ];
+
+      services.xserver.enable = true;
+
+      services.xserver.displayManager = {
+        gdm.enable = true;
+        gdm.debug = true;
+        autoLogin = {
+          enable = true;
+          user = user.name;
+        };
+      };
+
+      services.xserver.desktopManager.gnome.enable = true;
+      services.xserver.desktopManager.gnome.debug = true;
+      services.xserver.desktopManager.gnome.flashback.enableMetacity = true;
+      services.xserver.displayManager.defaultSession = "gnome-flashback-metacity";
+    };
+
+  testScript = { nodes, ... }: let
+    user = nodes.machine.config.users.users.alice;
+    uid = toString user.uid;
+    xauthority = "/run/user/${uid}/gdm/Xauthority";
+  in ''
+      with subtest("Login to GNOME Flashback with GDM"):
+          machine.wait_for_x()
+          # Wait for alice to be logged in"
+          machine.wait_for_unit("default.target", "${user.name}")
+          machine.wait_for_file("${xauthority}")
+          machine.succeed("xauth merge ${xauthority}")
+          # Check that logging in has given the user ownership of devices
+          assert "alice" in machine.succeed("getfacl -p /dev/snd/timer")
+
+      with subtest("Wait for Metacity"):
+          machine.wait_until_succeeds(
+              "pgrep metacity"
+          )
+          machine.sleep(20)
+          machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/gnome-xorg.nix b/nixos/tests/gnome-xorg.nix
index 618458b1f6b..d616d4f0235 100644
--- a/nixos/tests/gnome-xorg.nix
+++ b/nixos/tests/gnome-xorg.nix
@@ -24,7 +24,6 @@ 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" = {
@@ -61,10 +60,10 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
     # False when startup is done
     startingUp = su "${gdbus} ${eval} Main.layoutManager._startingUp";
 
-    # Start gnome-terminal
-    gnomeTerminalCommand = su "gnome-terminal";
+    # Start Console
+    launchConsole = su "${bus} gapplication launch org.gnome.Console";
 
-    # Hopefully gnome-terminal's wm class
+    # Hopefully Console's wm class
     wmClass = su "${gdbus} ${eval} global.display.focus_window.wm_class";
   in ''
       with subtest("Login to GNOME Xorg with GDM"):
@@ -82,13 +81,17 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
               "${startingUp} | grep -q 'true,..false'"
           )
 
-      with subtest("Open Gnome Terminal"):
+      with subtest("Open Console"):
+          # Close the Activities view so that Shell can correctly track the focused window.
+          machine.send_key("esc")
+
           machine.succeed(
-              "${gnomeTerminalCommand}"
+              "${launchConsole}"
           )
-          # correct output should be (true, '"Gnome-terminal"')
+          # correct output should be (true, '"kgx"')
+          # For some reason, this deviates from Wayland.
           machine.wait_until_succeeds(
-              "${wmClass} | grep -q  'true,...Gnome-terminal'"
+              "${wmClass} | grep -q  'true,...kgx'"
           )
           machine.sleep(20)
           machine.screenshot("screen")
diff --git a/nixos/tests/gnome.nix b/nixos/tests/gnome.nix
index 05619cbd7d8..5a28e3bb0e8 100644
--- a/nixos/tests/gnome.nix
+++ b/nixos/tests/gnome.nix
@@ -22,14 +22,6 @@ 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 {
-          name = "org.gnome.Terminal";
-          package = pkgs.gnome.gnome-terminal;
-        })
-      ];
 
       systemd.user.services = {
         "org.gnome.Shell@wayland" = {
@@ -64,10 +56,10 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
     # False when startup is done
     startingUp = su "${gdbus} ${eval} Main.layoutManager._startingUp";
 
-    # Start gnome-terminal
-    gnomeTerminalCommand = su "${bus} gnome-terminal";
+    # Start Console
+    launchConsole = su "${bus} gapplication launch org.gnome.Console";
 
-    # Hopefully gnome-terminal's wm class
+    # Hopefully Console's wm class
     wmClass = su "${gdbus} ${eval} global.display.focus_window.wm_class";
   in ''
       with subtest("Login to GNOME with GDM"):
@@ -86,10 +78,16 @@ import ./make-test-python.nix ({ pkgs, lib, ...} : {
               "${startingUp} | grep -q 'true,..false'"
           )
 
-      with subtest("Open Gnome Terminal"):
-          # correct output should be (true, '"gnome-terminal-server"')
+      with subtest("Open Console"):
+          # Close the Activities view so that Shell can correctly track the focused window.
+          machine.send_key("esc")
+
+          machine.succeed(
+              "${launchConsole}"
+          )
+          # correct output should be (true, '"org.gnome.Console"')
           machine.wait_until_succeeds(
-              "${wmClass} | grep -q 'gnome-terminal-server'"
+              "${wmClass} | grep -q 'true,...org.gnome.Console'"
           )
           machine.sleep(20)
           machine.screenshot("screen")
diff --git a/nixos/tests/gnupg.nix b/nixos/tests/gnupg.nix
new file mode 100644
index 00000000000..65a9a93007f
--- /dev/null
+++ b/nixos/tests/gnupg.nix
@@ -0,0 +1,118 @@
+import ./make-test-python.nix ({ pkgs, lib, ...}:
+
+{
+  name = "gnupg";
+  meta = with lib.maintainers; {
+    maintainers = [ rnhmjoj ];
+  };
+
+  # server for testing SSH
+  nodes.server = { ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    users.users.alice.isNormalUser = true;
+    services.openssh.enable = true;
+  };
+
+  # machine for testing GnuPG
+  nodes.machine = { pkgs, ... }: {
+    imports = [ ../modules/profiles/minimal.nix ];
+
+    users.users.alice.isNormalUser = true;
+    services.getty.autologinUser = "alice";
+
+    environment.shellInit = ''
+      # preset a key passphrase in gpg-agent
+      preset_key() {
+        # find all keys
+        case "$1" in
+          ssh) grips=$(awk '/^[0-9A-F]/{print $1}' "''${GNUPGHOME:-$HOME/.gnupg}/sshcontrol") ;;
+          pgp) grips=$(gpg --with-keygrip --list-secret-keys | awk '/Keygrip/{print $3}') ;;
+        esac
+
+        # try to preset the passphrase for each key found
+        for grip in $grips; do
+          "$(gpgconf --list-dirs libexecdir)/gpg-preset-passphrase" -c -P "$2" "$grip"
+        done
+      }
+    '';
+
+    programs.gnupg.agent.enable = true;
+    programs.gnupg.agent.enableSSHSupport = true;
+  };
+
+  testScript =
+    ''
+      import shlex
+
+
+      def as_alice(command: str) -> str:
+          """
+          Wraps a command to run it as Alice in a login shell
+          """
+          quoted = shlex.quote(command)
+          return "su --login alice --command " + quoted
+
+
+      start_all()
+
+      with subtest("Wait for the autologin"):
+          machine.wait_until_tty_matches("1", "alice@machine")
+
+      with subtest("Can generate a PGP key"):
+          # Note: this needs a tty because of pinentry
+          machine.send_chars("gpg --gen-key\n")
+          machine.wait_until_tty_matches("1", "Real name:")
+          machine.send_chars("Alice\n")
+          machine.wait_until_tty_matches("1", "Email address:")
+          machine.send_chars("alice@machine\n")
+          machine.wait_until_tty_matches("1", "Change")
+          machine.send_chars("O\n")
+          machine.wait_until_tty_matches("1", "Please enter")
+          machine.send_chars("pgp_p4ssphrase\n")
+          machine.wait_until_tty_matches("1", "Please re-enter")
+          machine.send_chars("pgp_p4ssphrase\n")
+          machine.wait_until_tty_matches("1", "public and secret key created")
+
+      with subtest("Confirm the key is in the keyring"):
+          machine.wait_until_succeeds(as_alice("gpg --list-secret-keys | grep -q alice@machine"))
+
+      with subtest("Can generate and add an SSH key"):
+          machine.succeed(as_alice("ssh-keygen -t ed25519 -f alice -N ssh_p4ssphrase"))
+
+          # Note: apparently this must be run before using the OpenSSH agent
+          # socket for the first time in a tty. It's not needed for `ssh`
+          # because there's a hook that calls it automatically (only in NixOS).
+          machine.send_chars("gpg-connect-agent updatestartuptty /bye\n")
+
+          # Note: again, this needs a tty because of pinentry
+          machine.send_chars("ssh-add alice\n")
+          machine.wait_until_tty_matches("1", "Enter passphrase")
+          machine.send_chars("ssh_p4ssphrase\n")
+          machine.wait_until_tty_matches("1", "Please enter")
+          machine.send_chars("ssh_agent_p4ssphrase\n")
+          machine.wait_until_tty_matches("1", "Please re-enter")
+          machine.send_chars("ssh_agent_p4ssphrase\n")
+
+      with subtest("Confirm the SSH key has been registered"):
+          machine.wait_until_succeeds(as_alice("ssh-add -l | grep -q alice@machine"))
+
+      with subtest("Can preset the key passphrases in the agent"):
+          machine.succeed(as_alice("echo allow-preset-passphrase > .gnupg/gpg-agent.conf"))
+          machine.succeed(as_alice("pkill gpg-agent"))
+          machine.succeed(as_alice("preset_key pgp pgp_p4ssphrase"))
+          machine.succeed(as_alice("preset_key ssh ssh_agent_p4ssphrase"))
+
+      with subtest("Can encrypt and decrypt a message"):
+          machine.succeed(as_alice("echo Hello | gpg -e -r alice | gpg -d | grep -q Hello"))
+
+      with subtest("Can log into the server"):
+          # Install Alice's public key
+          public_key = machine.succeed(as_alice("cat alice.pub"))
+          server.succeed("mkdir /etc/ssh/authorized_keys.d")
+          server.succeed(f"printf '{public_key}' > /etc/ssh/authorized_keys.d/alice")
+
+          server.wait_for_open_port(22)
+          machine.succeed(as_alice("ssh -i alice -o StrictHostKeyChecking=no server exit"))
+    '';
+})
diff --git a/nixos/tests/gollum.nix b/nixos/tests/gollum.nix
index 833db87f2f3..44d373e3526 100644
--- a/nixos/tests/gollum.nix
+++ b/nixos/tests/gollum.nix
@@ -9,6 +9,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
   testScript = { nodes, ... }: ''
     webserver.wait_for_unit("gollum")
-    webserver.wait_for_open_port(${toString nodes.webserver.config.services.gollum.port})
+    webserver.wait_for_open_port(${toString nodes.webserver.services.gollum.port})
   '';
 })
diff --git a/nixos/tests/gonic.nix b/nixos/tests/gonic.nix
new file mode 100644
index 00000000000..726d7da0970
--- /dev/null
+++ b/nixos/tests/gonic.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "gonic";
+
+  nodes.machine = { ... }: {
+    services.gonic = {
+      enable = true;
+      settings = {
+        music-path = [ "/tmp" ];
+        podcast-path = "/tmp";
+      };
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("gonic")
+    machine.wait_for_open_port(4747)
+  '';
+})
diff --git a/nixos/tests/google-oslogin/server.nix b/nixos/tests/google-oslogin/server.nix
index faf5e847d7e..3df41155c92 100644
--- a/nixos/tests/google-oslogin/server.nix
+++ b/nixos/tests/google-oslogin/server.nix
@@ -17,8 +17,8 @@ in {
   };
 
   services.openssh.enable = true;
-  services.openssh.kbdInteractiveAuthentication = false;
-  services.openssh.passwordAuthentication = false;
+  services.openssh.settings.KbdInteractiveAuthentication = false;
+  services.openssh.settings.PasswordAuthentication = false;
 
   security.googleOsLogin.enable = true;
 
diff --git a/nixos/tests/google-oslogin/server.py b/nixos/tests/google-oslogin/server.py
index 5ea9bbd2c96..622cd86b261 100755
--- a/nixos/tests/google-oslogin/server.py
+++ b/nixos/tests/google-oslogin/server.py
@@ -103,6 +103,16 @@ class ReqHandler(BaseHTTPRequestHandler):
             self._send_json_ok(gen_mockuser(username=username, uid=uid, gid=uid, home_directory=f"/home/{username}", snakeoil_pubkey=SNAKEOIL_PUBLIC_KEY))
             return
 
+        # we need to provide something at the groups endpoint.
+        # the nss module does segfault if we don't.
+        elif pu.path == "/computeMetadata/v1/oslogin/groups":
+            self._send_json_ok({
+                "posixGroups": [
+                    {"name" : "demo", "gid" : 4294967295}
+                ],
+            })
+            return
+
         # authorize endpoint
         elif pu.path == "/computeMetadata/v1/oslogin/authorize":
             # is user allowed to login?
diff --git a/nixos/tests/grafana/provision/default.nix b/nixos/tests/grafana/provision/default.nix
index 1eb927632eb..96378452ade 100644
--- a/nixos/tests/grafana/provision/default.nix
+++ b/nixos/tests/grafana/provision/default.nix
@@ -22,9 +22,15 @@ let
       };
     };
 
-    systemd.tmpfiles.rules = [
-      "L /var/lib/grafana/dashboards/test.json 0700 grafana grafana - ${pkgs.writeText "test.json" (builtins.readFile ./test_dashboard.json)}"
-    ];
+    system.activationScripts.setup-grafana = {
+      deps = [ "users" ];
+      text = ''
+        mkdir -p /var/lib/grafana/dashboards
+        chown -R grafana:grafana /var/lib/grafana
+        chmod 0700 -R /var/lib/grafana/dashboards
+        cp ${pkgs.writeText "test.json" (builtins.readFile ./test_dashboard.json)} /var/lib/grafana/dashboards/
+      '';
+    };
   };
 
   extraNodeConfs = {
diff --git a/nixos/tests/graylog.nix b/nixos/tests/graylog.nix
index 23f426fc7af..3f7cc3a9143 100644
--- a/nixos/tests/graylog.nix
+++ b/nixos/tests/graylog.nix
@@ -8,7 +8,6 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
 
     services.mongodb.enable = true;
     services.elasticsearch.enable = true;
-    services.elasticsearch.package = pkgs.elasticsearch-oss;
     services.elasticsearch.extraConf = ''
       network.publish_host: 127.0.0.1
       network.bind_host: 127.0.0.1
diff --git a/nixos/tests/hadoop/hbase.nix b/nixos/tests/hadoop/hbase.nix
index d9d2dac0f65..0416345682a 100644
--- a/nixos/tests/hadoop/hbase.nix
+++ b/nixos/tests/hadoop/hbase.nix
@@ -53,6 +53,24 @@ with pkgs.lib;
         };
       };
     };
+    thrift = { ... }:{
+      services.hadoop = {
+        inherit coreSite;
+        hbase = {
+          inherit zookeeperQuorum;
+          thrift = defOpts;
+        };
+      };
+    };
+    rest = { ... }:{
+      services.hadoop = {
+        inherit coreSite;
+        hbase = {
+          inherit zookeeperQuorum;
+          rest = defOpts;
+        };
+      };
+    };
   };
 
   testScript = ''
@@ -80,5 +98,12 @@ with pkgs.lib;
     assert "1 active master, 0 backup masters, 1 servers" in master.succeed("echo status | HADOOP_USER_NAME=hbase hbase shell -n")
     regionserver.wait_until_succeeds("echo \"create 't1','f1'\" | HADOOP_USER_NAME=hbase hbase shell -n")
     assert "NAME => 'f1'" in regionserver.succeed("echo \"describe 't1'\" | HADOOP_USER_NAME=hbase hbase shell -n")
+
+    rest.wait_for_open_port(8080)
+    assert "${hbase.version}" in regionserver.succeed("curl http://rest:8080/version/cluster")
+
+    thrift.wait_for_open_port(9090)
   '';
+
+  meta.maintainers = with maintainers; [ illustris ];
 })
diff --git a/nixos/tests/haproxy.nix b/nixos/tests/haproxy.nix
index b6ff4102fe6..555474d7f29 100644
--- a/nixos/tests/haproxy.nix
+++ b/nixos/tests/haproxy.nix
@@ -2,7 +2,6 @@ import ./make-test-python.nix ({ pkgs, ...}: {
   name = "haproxy";
   nodes = {
     machine = { ... }: {
-      imports = [ ../modules/profiles/minimal.nix ];
       services.haproxy = {
         enable = true;
         config = ''
diff --git a/nixos/tests/headscale.nix b/nixos/tests/headscale.nix
new file mode 100644
index 00000000000..48658b5dade
--- /dev/null
+++ b/nixos/tests/headscale.nix
@@ -0,0 +1,17 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "headscale";
+  meta.maintainers = with lib.maintainers; [ misterio77 ];
+
+  nodes.machine = { ... }: {
+    services.headscale.enable = true;
+    environment.systemPackages = [ pkgs.headscale ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("headscale")
+    machine.wait_for_open_port(8080)
+    # Test basic funcionality
+    machine.succeed("headscale namespaces create test")
+    machine.succeed("headscale preauthkeys -n test create")
+  '';
+})
diff --git a/nixos/tests/hibernate.nix b/nixos/tests/hibernate.nix
index cb75322ca5f..4d0b53e95b3 100644
--- a/nixos/tests/hibernate.nix
+++ b/nixos/tests/hibernate.nix
@@ -63,7 +63,7 @@ in makeTest {
         # Small root disk for installer
         512
       ];
-      virtualisation.bootDevice = "/dev/vdb";
+      virtualisation.rootDevice = "/dev/vdb";
     };
   };
 
diff --git a/nixos/tests/home-assistant.nix b/nixos/tests/home-assistant.nix
index 8d58de75eab..22a63cc7366 100644
--- a/nixos/tests/home-assistant.nix
+++ b/nixos/tests/home-assistant.nix
@@ -22,22 +22,23 @@ in {
       enable = true;
       inherit configDir;
 
-      # tests loading components by overriding the package
+      # provide dependencies through package overrides
       package = (pkgs.home-assistant.override {
         extraPackages = ps: with ps; [
           colorama
         ];
-        extraComponents = [ "zha" ];
-      }).overrideAttrs (oldAttrs: {
-        doInstallCheck = false;
+        extraComponents = [
+          # test char-tty device allow propagation into the service
+          "zha"
+         ];
       });
 
-      # tests loading components from the module
+      # provide component dependencies explicitly from the module
       extraComponents = [
-        "wake_on_lan"
+        "mqtt"
       ];
 
-      # test extra package passing from the module
+      # provide package for postgresql support
       extraPackages = python3Packages: with python3Packages; [
         psycopg2
       ];
@@ -60,6 +61,13 @@ in {
         # https://www.home-assistant.io/integrations/frontend/
         frontend = {};
 
+        # include some popular integrations, that absolutely shouldn't break
+        esphome = {};
+        knx = {};
+        matter = {};
+        shelly = {};
+        zha = {};
+
         # 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/
@@ -106,41 +114,43 @@ in {
     # 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 = {};
+      configuration.services.home-assistant.config.backup = {};
     };
   };
 
   testScript = { nodes, ... }: let
-    system = nodes.hass.config.system.build.toplevel;
+    system = nodes.hass.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 overridden 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")
+    def get_journal_cursor() -> str:
+        exit, out = hass.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'")
+    def get_journal_since(cursor) -> str:
+        exit, out = hass.execute(f"journalctl --after-cursor='{cursor}' -u home-assistant.service")
+        assert exit == 0
+        return out
+
+
+    def get_unit_property(property) -> str:
+        exit, out = hass.execute(f"systemctl show --property={property} home-assistant.service")
+        assert exit == 0
+        return out
+
+
+    def wait_for_homeassistant(cursor):
+        hass.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)
+    cursor = get_journal_cursor()
 
     with subtest("Check that YAML configuration file is in place"):
         hass.succeed("test -L ${configDir}/configuration.yaml")
@@ -148,19 +158,22 @@ in {
     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)
+        wait_for_homeassistant(cursor)
         hass.wait_for_open_port(8123)
         hass.succeed("curl --fail http://localhost:8123/lovelace")
 
+    with subtest("Check that optional dependencies are in the PYTHONPATH"):
+        env = get_unit_property("Environment")
+        python_path = env.split("PYTHONPATH=")[1].split()[0]
+        for package in ["colorama", "paho-mqtt", "psycopg2"]:
+            assert package in python_path, f"{package} not in PYTHONPATH"
+
+    with subtest("Check that declaratively configured components get setup"):
+        journal = get_journal_since(cursor)
+        for domain in ["emulated_hue", "wake_on_lan"]:
+            assert f"Setup of domain {domain} took" in journal, f"{domain} setup missing"
+
     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")
@@ -169,25 +182,28 @@ in {
         hass.succeed("systemctl show -p DeviceAllow home-assistant.service | grep -q char-ttyUSB")
 
     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)
+        pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
+        cursor = get_journal_cursor()
+        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(cursor)
+
+    with subtest("Check service restarts when dependencies change"):
+        pid = new_pid
+        cursor = get_journal_cursor()
+        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 should change when its PYTHONPATH changess"
+        wait_for_homeassistant(cursor)
+
+    with subtest("Check that new components get setup after restart"):
+        journal = get_journal_since(cursor)
+        for domain in ["esphome"]:
+            assert f"Setup of domain {domain} took" in journal, f"{domain} setup missing"
 
     with subtest("Check that no errors were logged"):
-        output_log = hass.succeed("cat ${configDir}/home-assistant.log")
-        assert "ERROR" not in output_log
+        hass.fail("journalctl -u home-assistant -o cat | grep -q ERROR")
 
     with subtest("Check systemd unit hardening"):
         hass.log(hass.succeed("systemctl cat home-assistant.service"))
diff --git a/nixos/tests/hostname.nix b/nixos/tests/hostname.nix
index 1de8f19267a..6122e2ffeb8 100644
--- a/nixos/tests/hostname.nix
+++ b/nixos/tests/hostname.nix
@@ -1,6 +1,6 @@
-{ system ? builtins.currentSystem,
-  config ? {},
-  pkgs ? import ../.. { inherit system config; }
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
 }:
 
 with import ../lib/testing-python.nix { inherit system pkgs; };
@@ -14,55 +14,55 @@ let
         let res = builtins.tryEval str;
         in if (res.success && res.value != null) then res.value else "null";
     in
-      makeTest {
-        name = "hostname-${fqdn}";
-        meta = with pkgs.lib.maintainers; {
-          maintainers = [ primeos blitz ];
-        };
+    makeTest {
+      name = "hostname-${fqdn}";
+      meta = with pkgs.lib.maintainers; {
+        maintainers = [ primeos blitz ];
+      };
 
-        nodes.machine = { lib, ... }: {
-          networking.hostName = hostName;
-          networking.domain = domain;
+      nodes.machine = { lib, ... }: {
+        networking.hostName = hostName;
+        networking.domain = domain;
 
-          environment.systemPackages = with pkgs; [
-            inetutils
-          ];
-        };
+        environment.systemPackages = with pkgs; [
+          inetutils
+        ];
+      };
 
-        testScript = { nodes, ... }: ''
-          start_all()
+      testScript = { nodes, ... }: ''
+        start_all()
 
-          machine = ${hostName}
+        machine = ${hostName}
 
-          machine.wait_for_unit("network-online.target")
+        machine.wait_for_unit("network-online.target")
 
-          # Test if NixOS computes the correct FQDN (either a FQDN or an error/null):
-          assert "${getStr nodes.machine.config.networking.fqdn}" == "${getStr fqdnOrNull}"
+        # Test if NixOS computes the correct FQDN (either a FQDN or an error/null):
+        assert "${getStr nodes.machine.networking.fqdn}" == "${getStr fqdnOrNull}"
 
-          # The FQDN, domain name, and hostname detection should work as expected:
-          assert "${fqdn}" == machine.succeed("hostname --fqdn").strip()
-          assert "${optionalString (domain != null) domain}" == machine.succeed("dnsdomainname").strip()
-          assert (
-              "${hostName}"
-              == machine.succeed(
-                  'hostnamectl status | grep "Static hostname" | cut -d: -f2'
-              ).strip()
-          )
+        # The FQDN, domain name, and hostname detection should work as expected:
+        assert "${fqdn}" == machine.succeed("hostname --fqdn").strip()
+        assert "${optionalString (domain != null) domain}" == machine.succeed("dnsdomainname").strip()
+        assert (
+            "${hostName}"
+            == machine.succeed(
+                'hostnamectl status | grep "Static hostname" | cut -d: -f2'
+            ).strip()
+        )
 
-          # 127.0.0.1 and ::1 should resolve back to "localhost":
-          assert (
-              "localhost" == machine.succeed("getent hosts 127.0.0.1 | awk '{print $2}'").strip()
-          )
-          assert "localhost" == machine.succeed("getent hosts ::1 | awk '{print $2}'").strip()
+        # 127.0.0.1 and ::1 should resolve back to "localhost":
+        assert (
+            "localhost" == machine.succeed("getent hosts 127.0.0.1 | awk '{print $2}'").strip()
+        )
+        assert "localhost" == machine.succeed("getent hosts ::1 | awk '{print $2}'").strip()
 
-          # 127.0.0.2 should resolve back to the FQDN and hostname:
-          fqdn_and_host_name = "${optionalString (domain != null) "${hostName}.${domain} "}${hostName}"
-          assert (
-              fqdn_and_host_name
-              == machine.succeed("getent hosts 127.0.0.2 | awk '{print $2,$3}'").strip()
-          )
-        '';
-      };
+        # 127.0.0.2 should resolve back to the FQDN and hostname:
+        fqdn_and_host_name = "${optionalString (domain != null) "${hostName}.${domain} "}${hostName}"
+        assert (
+            fqdn_and_host_name
+            == machine.succeed("getent hosts 127.0.0.2 | awk '{print $2,$3}'").strip()
+        )
+      '';
+    };
 
 in
 {
diff --git a/nixos/tests/ihatemoney/default.nix b/nixos/tests/ihatemoney/default.nix
deleted file mode 100644
index 894a97d43d3..00000000000
--- a/nixos/tests/ihatemoney/default.nix
+++ /dev/null
@@ -1,71 +0,0 @@
-{ system ? builtins.currentSystem,
-  config ? {},
-  pkgs ? import ../../.. { inherit system config; }
-}:
-
-let
-  inherit (import ../../lib/testing-python.nix { inherit system pkgs; }) makeTest;
-  f = backend: makeTest {
-    name = "ihatemoney-${backend}";
-    nodes.machine = { nodes, lib, ... }: {
-      services.ihatemoney = {
-        enable = true;
-        enablePublicProjectCreation = true;
-        secureCookie = false;
-        inherit backend;
-        uwsgiConfig = {
-          http = ":8000";
-        };
-      };
-      boot.cleanTmpDir = true;
-      # for exchange rates
-      security.pki.certificateFiles = [ ./server.crt ];
-      networking.extraHosts = "127.0.0.1 api.exchangerate.host";
-      services.nginx = {
-        enable = true;
-        virtualHosts."api.exchangerate.host" = {
-          addSSL = true;
-          # openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 1000000 -nodes -subj '/CN=api.exchangerate.host'
-          sslCertificate = ./server.crt;
-          sslCertificateKey = ./server.key;
-          locations."/".return = "200 '${builtins.readFile ./rates.json}'";
-        };
-      };
-      # ihatemoney needs a local smtp server otherwise project creation just crashes
-      services.postfix.enable = true;
-    };
-    testScript = ''
-      machine.wait_for_open_port(8000)
-      machine.wait_for_unit("uwsgi.service")
-      machine.wait_until_succeeds("curl --fail https://api.exchangerate.host")
-      machine.wait_until_succeeds("curl --fail http://localhost:8000")
-
-      result = machine.succeed(
-          "curl --fail -X POST http://localhost:8000/api/projects -d 'name=yay&id=yay&password=yay&contact_email=yay@example.com&default_currency=XXX'"
-      )
-      assert '"yay"' in result, repr(result)
-      owner, timestamp = machine.succeed(
-          "stat --printf %U:%G___%Y /var/lib/ihatemoney/secret_key"
-      ).split("___")
-      assert "ihatemoney:ihatemoney" == owner
-
-      with subtest("Restart machine and service"):
-          machine.shutdown()
-          machine.start()
-          machine.wait_for_open_port(8000)
-          machine.wait_for_unit("uwsgi.service")
-
-      with subtest("check that the database is really persistent"):
-          machine.succeed("curl --fail --basic -u yay:yay http://localhost:8000/api/projects/yay")
-
-      with subtest("check that the secret key is really persistent"):
-          timestamp2 = machine.succeed("stat --printf %Y /var/lib/ihatemoney/secret_key")
-          assert timestamp == timestamp2
-
-      assert "ihatemoney" in machine.succeed("curl --fail http://localhost:8000")
-    '';
-  };
-in {
-  ihatemoney-sqlite = f "sqlite";
-  ihatemoney-postgresql = f "postgresql";
-}
diff --git a/nixos/tests/ihatemoney/rates.json b/nixos/tests/ihatemoney/rates.json
deleted file mode 100644
index ebdd2651b04..00000000000
--- a/nixos/tests/ihatemoney/rates.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
-  "rates": {
-    "CAD": 1.3420055134,
-    "HKD": 7.7513783598,
-    "ISK": 135.9407305307,
-    "PHP": 49.3762922123,
-    "DKK": 6.4126464507,
-    "HUF": 298.9145416954,
-    "CZK": 22.6292212267,
-    "GBP": 0.7838128877,
-    "RON": 4.1630771881,
-    "SEK": 8.8464851826,
-    "IDR": 14629.5658166782,
-    "INR": 74.8328738801,
-    "BRL": 5.2357856651,
-    "RUB": 71.8416609235,
-    "HRK": 6.4757064094,
-    "JPY": 106.2715368711,
-    "THB": 31.7203652653,
-    "CHF": 0.9243625086,
-    "EUR": 0.8614748449,
-    "MYR": 4.2644727774,
-    "BGN": 1.6848725017,
-    "TRY": 6.8483804273,
-    "CNY": 7.0169710544,
-    "NOK": 9.213731909,
-    "NZD": 1.5080978635,
-    "ZAR": 16.7427636113,
-    "USD": 1,
-    "MXN": 22.4676085458,
-    "SGD": 1.3855099931,
-    "AUD": 1.4107512061,
-    "ILS": 3.4150585803,
-    "KRW": 1203.3339076499,
-    "PLN": 3.794452102
-  },
-  "base": "USD",
-  "date": "2020-07-24"
-}
diff --git a/nixos/tests/ihatemoney/server.crt b/nixos/tests/ihatemoney/server.crt
deleted file mode 100644
index 10e568b14b1..00000000000
--- a/nixos/tests/ihatemoney/server.crt
+++ /dev/null
@@ -1,28 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIEvjCCAqYCCQDkTQrENPCZjjANBgkqhkiG9w0BAQsFADAgMR4wHAYDVQQDDBVh
-cGkuZXhjaGFuZ2VyYXRlLmhvc3QwIBcNMjEwNzE0MTI1MzQ0WhgPNDc1OTA2MTEx
-MjUzNDRaMCAxHjAcBgNVBAMMFWFwaS5leGNoYW5nZXJhdGUuaG9zdDCCAiIwDQYJ
-KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL5zpwUYa/ySqvJ/PUnXYsl1ww5SNGJh
-NujCRxC0Gw+5t5O7USSHRdz7Eb2PNFMa7JR+lliLAWdjHfqPXJWmP10X5ebvyxeQ
-TJkR1HpDSY6TQQlJvwr/JNGryyoQYjXvnyeyVu4TS3U0TTI631OonDAj+HbFIs9L
-gr/HfHzFmxRVLwaJ7hebanihc5RzoWTxgswiOwYQu5AivXQqcvUIxELeT7CxWwiw
-be/SlalDgoezB/poqaa215FUuN2av+nTn+swH3WOi9kwePLgVKn9BnDMwyh8et13
-yt27RWCSOcZagRSYsSbBaEJbClZvnuYvDqooJEy0GVbGBZpClKRKe92yd0PTf3ZJ
-GupyNoCFQlGugY//WLrsPv/Q4WwP+qZ6t97sV0CdM+epKVde/LfPKn+tFMv86qIg
-Q/uGHdDwUI8XH2EysAavhdlssSrovmpl4hyo9UkzTWfJgAbmOZY3Vba41wsq12FT
-usDsswGLBD10MdXWltR/Hdk8OnosLmeJxfZODAv31KSfd+4b6Ntr9BYQvAQSO+1/
-Mf7gEQtNhO003VKIyV5cpH4kVQieEcvoEKgq32NVBSKVf6UIPWIefu19kvrttaUu
-Q2QW2Qm4Ph/4cWpxl0jcrN5rjmgaBtIMmKYjRIS0ThDWzfVkJdmJuATzExJAplLN
-nYPBG3gOtQQpAgMBAAEwDQYJKoZIhvcNAQELBQADggIBAJzt/aN7wl88WrvBasVi
-fSJmJjRaW2rYyBUMptQNkm9ElHN2eQQxJgLi8+9ArQxuGKhHx+D1wMGF8w2yOp0j
-4atfbXDcT+cTQY55qdEeYgU8KhESHHGszGsUpv7hzU2cACZiXG0YbOmORFYcn49Z
-yPyN98kW8BViLzNF9v+I/NJPuaaCeWKjXCqY2GCzddiuotrlLtz0CODXZJ506I1F
-38vQgZb10yAe6+R4y0BK7sUlmfr9BBqVcDQ/z74Kph1aB32zwP8KrNitwG1Tyk6W
-rxD1dStEQyX8uDPAspe2JrToMWsOMje9F5lotmuzyvwRJYfAav300EtIggBqpiHR
-o0P/1xxBzmaCHxEUJegdoYg8Q27llqsjR2T78uv/BlxpX9Dv5kNex5EZThKqyz4a
-Fn1VqiA3D9IsvxH4ud+8eDaP24u1yYObSTDIBsw9xDvoV8fV+NWoNNhcAL5GwC0P
-Goh7/brZSHUprxGpwRB524E//8XmCsRd/+ShtXbi4gEODMH4xLdkD7fZIJC4eG1H
-GOVc1MwjiYvbQlPs6MOcQ0iKQneSlaEJmyyO5Ro5OKiKj89Az/mLYX3R17AIsu0T
-Q5pGcmhKVRyu0zXvkGfK352TLwoe+4vbmakDq21Pkkcy8V9M4wP+vpCfQkg1REQ1
-+mr1Vg+SFya3mlCxpFTy3j8E
------END CERTIFICATE-----
diff --git a/nixos/tests/ihatemoney/server.key b/nixos/tests/ihatemoney/server.key
deleted file mode 100644
index 72a43577d64..00000000000
--- a/nixos/tests/ihatemoney/server.key
+++ /dev/null
@@ -1,52 +0,0 @@
------BEGIN PRIVATE KEY-----
-MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC+c6cFGGv8kqry
-fz1J12LJdcMOUjRiYTbowkcQtBsPubeTu1Ekh0Xc+xG9jzRTGuyUfpZYiwFnYx36
-j1yVpj9dF+Xm78sXkEyZEdR6Q0mOk0EJSb8K/yTRq8sqEGI1758nslbuE0t1NE0y
-Ot9TqJwwI/h2xSLPS4K/x3x8xZsUVS8Gie4Xm2p4oXOUc6Fk8YLMIjsGELuQIr10
-KnL1CMRC3k+wsVsIsG3v0pWpQ4KHswf6aKmmtteRVLjdmr/p05/rMB91jovZMHjy
-4FSp/QZwzMMofHrdd8rdu0VgkjnGWoEUmLEmwWhCWwpWb57mLw6qKCRMtBlWxgWa
-QpSkSnvdsndD0392SRrqcjaAhUJRroGP/1i67D7/0OFsD/qmerfe7FdAnTPnqSlX
-Xvy3zyp/rRTL/OqiIEP7hh3Q8FCPFx9hMrAGr4XZbLEq6L5qZeIcqPVJM01nyYAG
-5jmWN1W2uNcLKtdhU7rA7LMBiwQ9dDHV1pbUfx3ZPDp6LC5nicX2TgwL99Skn3fu
-G+jba/QWELwEEjvtfzH+4BELTYTtNN1SiMleXKR+JFUInhHL6BCoKt9jVQUilX+l
-CD1iHn7tfZL67bWlLkNkFtkJuD4f+HFqcZdI3Kzea45oGgbSDJimI0SEtE4Q1s31
-ZCXZibgE8xMSQKZSzZ2DwRt4DrUEKQIDAQABAoICAQCpwU465XTDUTvcH/vSCJB9
-/2BYMH+OvRYDS7+qLM7+Kkxt+oWt6IEmIgfDDZTXCmWbSmXaEDS1IYzEG+qrXN6X
-rMh4Gn7MxwrvWQwp2jYDRk+u5rPJKnh4Bwd0u9u+NZKIAJcpZ7tXgcHZJs6Os/hb
-lIRP4RFQ8f5d0IKueDftXKwoyOKW2imB0m7CAHr4DajHKS+xDVMRe1Wg6IFE1YaS
-D7O6S6tXyGKFZA+QKqN7LuHKmmW1Or5URM7uf5PV6JJfQKqZzu/qLCFyYvA0AFsw
-SeMeAC5HnxIMp3KETHIA0gTCBgPJBpVWp+1D9AQPKhyJIHSShekcBi9SO0xgUB+s
-h1UEcC2zf95Vson0KySX9zWRUZkrU8/0KYhYljN2/vdW8XxkRBC0pl3xWzq2kMgz
-SscZqI/MzyeUHaQno62GRlWn+WKP2NidDfR0Td/ybge1DJX+aDIfjalfCEIbJeqm
-BHn0CZ5z1RofatDlPj4p8+f2Trpcz/JCVKbGiQXi/08ZlCwkSIiOIcBVvAFErWop
-GJOBDU3StS/MXhQVb8ZeCkPBz0TM24Sv1az/MuW4w8gavpQuBC4aD5zY/TOwG8ei
-6S1sAZ0G2uc1A0FOngNvOyYYv+LImZKkWGXrLCRsqq6o/mh3M8bCHEY/lOZW8ZpL
-FCsDOO8deVZl/OX1VtB0bQKCAQEA3qRWDlUpCAU8BKa5Z1oRUz06e5KD58t2HpG8
-ndM3UO/F1XNB/6OGMWpL/XuBKOnWIB39UzsnnEtehKURTqqAsB1K3JQ5Q/FyuXRj
-+o7XnNXe5lHBL5JqBIoESDchSAooQhBlQSjLSL2lg//igk0puv08wMK7UtajkV7U
-35WDa6ks6jfoSeuVibfdobkTgfw5edirOBE2Q0U2KtGsnyAzsM6tRbtgI1Yhg7eX
-nSIc4IYgq2hNLBKsegeiz1w4M6O4CQDVYFWKHyKpdrvj/fG7YZMr6YtTkuC+QPDK
-mmQIEL/lj8E26MnPLKtnTFc06LQry2V3pLWNf4mMLPNLEupEXwKCAQEA2vyg8Npn
-EZRunIr51rYScC6U6iryDjJWCwJxwr8vGU+bkqUOHTl3EqZOi5tDeYJJ+WSBqjfW
-IWrPRFZzTITlAslZ02DQ5enS9PwgUUjl7LUEbHHh+fSNIgkVfDhsuNKFzcEaIM1X
-Dl4lI2T8jEzmBep+k8f6gNmgKBgqlCf7XraorIM5diLFzy2G10zdOQTw5hW3TsVY
-d968YpfC5j57/hCrf36ahIT7o1vxLD+L27Mm9Eiib45woWjaAR1Nc9kUjqY4yV7t
-3QOw/Id9+/Sx5tZftOBvHlFyz23e1yaI3VxsiLDO9RxJwAKyA+KOvAybE2VU28hI
-s5tAYOMV6BpEdwKCAQBqRIQyySERi/YOvkmGdC4KzhHJA7DkBXA2vRcLOdKQVjHW
-ZPIeg728fmEQ90856QrkP4w3mueYKT1PEL7HDojoBsNBr5n5vRgmPtCtulpdqJOA
-2YrdGwRxcDMFCRNgoECA7/R0enU1HhgPfiZuTUha0R6bXxcsPfjKnTn8EhAtZg1j
-KhY8mi7BEjq+Q2l1RJ9mci2fUE/XIgTtwTCkrykc/jkkLICBvU234fyC6tJftIWJ
-avpSzAL5KAXk9b55n25rFbPDDHEl1VSPsLTs8+GdfDKcgXz9gTouIwCBWreizwVS
-bUW5LQIu7w0aGhHN9JlmtuK5glKsikmW9vVhbOH/AoIBAE//O7fgwQguBh5Psqca
-CjBLBAFrQNOo1b/d27r95nHDoBx5CWfppzL75/Od+4825lkhuzB4h1Pb1e2r+yC3
-54UWEydh1c43leYC+LdY/w1yrzQCgj+yc6A8W0nuvuDhnxmj8iyLdsL752s/p/aE
-3P7KRAUuZ7eMSLJ86YkH9g8KgSHMKkCawVJG2lxqauI6iNo0kqtG8mOPzZfiwsMj
-jl4ors27bSz9+4MYwkicyjWvA4r3wcco7MI6MHF5x+KLKbRWyqXddN1pTM1jncVe
-BWNDauEDn/QeYuedxmsoW5Up/0gL9v6Zn+Nx2KAMsoHFxRzXxqEnUE+0Zlc+fbE1
-b08CggEBAMiZmWtRmfueu9NMh6mgs+cmMA1ZHmbnIbtFpVjc37lrKUcjLzGF3tmp
-zQl2wy8IcHpNv8F9aKhwAInxD49RUjyqvRD6Pru+EWN6gOPJIUVuZ6mvaf7BOxbn
-Rve63hN5k4znQ1MOqGRiUkBxYSJ5wnFyQP0/8Y6+JM5uAuRUcKVNyoGURpfMrmB3
-r+KHWltM9/5iIfiDNhwStFiuOJj1YBJVzrcAn8Zh5Q0+s1hXoOUs4doLcaPHTCTU
-3hyX78yROMcZto0pVzxgQrYz31yQ5ocy9WcOYbPbQ5gdlnBEv8d7umNY1siz2wkI
-NaEkKVO0D0jFtk37s/YqJpCsXg/B7yc=
------END PRIVATE KEY-----
diff --git a/nixos/tests/image-contents.nix b/nixos/tests/image-contents.nix
index 90908968a7e..858f7d8c68f 100644
--- a/nixos/tests/image-contents.nix
+++ b/nixos/tests/image-contents.nix
@@ -27,13 +27,19 @@ let
     inherit pkgs config;
     lib = pkgs.lib;
     format = "qcow2";
-    contents = [{
-      source = pkgs.writeText "testFile" "contents";
-      target = "/testFile";
-      user = "1234";
-      group = "5678";
-      mode = "755";
-    }];
+    contents = [
+      {
+        source = pkgs.writeText "testFile" "contents";
+        target = "/testFile";
+        user = "1234";
+        group = "5678";
+        mode = "755";
+      }
+      {
+        source = ./.;
+        target = "/testDir";
+      }
+    ];
   }) + "/nixos.qcow2";
 
 in makeEc2Test {
@@ -42,10 +48,15 @@ in makeEc2Test {
   userData = null;
   script = ''
     machine.start()
+    # Test that if contents includes a file, it is copied to the target.
     assert "content" in machine.succeed("cat /testFile")
     fileDetails = machine.succeed("ls -l /testFile")
     assert "1234" in fileDetails
     assert "5678" in fileDetails
     assert "rwxr-xr-x" in fileDetails
+
+    # Test that if contents includes a directory, it is copied to the target.
+    dirList = machine.succeed("ls /testDir")
+    assert "image-contents.nix" in dirList
   '';
 }
diff --git a/nixos/tests/initrd-luks-empty-passphrase.nix b/nixos/tests/initrd-luks-empty-passphrase.nix
new file mode 100644
index 00000000000..d2805f2f173
--- /dev/null
+++ b/nixos/tests/initrd-luks-empty-passphrase.nix
@@ -0,0 +1,97 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. {inherit system config; }
+, systemdStage1 ? false }:
+import ./make-test-python.nix ({ lib, pkgs, ... }: let
+
+  keyfile = pkgs.writeText "luks-keyfile" ''
+    MIGHAoGBAJ4rGTSo/ldyjQypd0kuS7k2OSsmQYzMH6TNj3nQ/vIUjDn7fqa3slt2
+    gV6EK3TmTbGc4tzC1v4SWx2m+2Bjdtn4Fs4wiBwn1lbRdC6i5ZYCqasTWIntWn+6
+    FllUkMD5oqjOR/YcboxG8Z3B5sJuvTP9llsF+gnuveWih9dpbBr7AgEC
+  '';
+
+in {
+  name = "initrd-luks-empty-passphrase";
+
+  nodes.machine = { pkgs, ... }: {
+    virtualisation = {
+      emptyDiskImages = [ 512 ];
+      useBootLoader = true;
+      useEFIBoot = true;
+    };
+
+    boot.loader.systemd-boot.enable = true;
+    boot.initrd.systemd = lib.mkIf systemdStage1 {
+      enable = true;
+      emergencyAccess = true;
+    };
+    environment.systemPackages = with pkgs; [ cryptsetup ];
+
+    specialisation.boot-luks-wrong-keyfile.configuration = {
+      boot.initrd.luks.devices = lib.mkVMOverride {
+        cryptroot = {
+          device = "/dev/vdb";
+          keyFile = "/etc/cryptroot.key";
+          tryEmptyPassphrase = true;
+          fallbackToPassword = !systemdStage1;
+        };
+      };
+      virtualisation.rootDevice = "/dev/mapper/cryptroot";
+      boot.initrd.secrets."/etc/cryptroot.key" = keyfile;
+    };
+
+    specialisation.boot-luks-missing-keyfile.configuration = {
+      boot.initrd.luks.devices = lib.mkVMOverride {
+        cryptroot = {
+          device = "/dev/vdb";
+          keyFile = "/etc/cryptroot.key";
+          tryEmptyPassphrase = true;
+          fallbackToPassword = !systemdStage1;
+        };
+      };
+      virtualisation.rootDevice = "/dev/mapper/cryptroot";
+    };
+  };
+
+  testScript = ''
+    # Encrypt key with empty key so boot should try keyfile and then fallback to empty passphrase
+
+
+    def grub_select_boot_luks_wrong_key_file():
+        """
+        Selects "boot-luks" from the GRUB menu
+        to trigger a login request.
+        """
+        machine.send_monitor_command("sendkey down")
+        machine.send_monitor_command("sendkey down")
+        machine.send_monitor_command("sendkey ret")
+
+    def grub_select_boot_luks_missing_key_file():
+        """
+        Selects "boot-luks" from the GRUB menu
+        to trigger a login request.
+        """
+        machine.send_monitor_command("sendkey down")
+        machine.send_monitor_command("sendkey ret")
+
+    # Create encrypted volume
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("echo "" | cryptsetup luksFormat /dev/vdb --batch-mode")
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks-wrong-keyfile.conf")
+    machine.succeed("sync")
+    machine.crash()
+
+    # Check if rootfs is on /dev/mapper/cryptroot
+    machine.wait_for_unit("multi-user.target")
+    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
+
+    # Choose boot-luks-missing-keyfile specialisation
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks-missing-keyfile.conf")
+    machine.succeed("sync")
+    machine.crash()
+
+    # Check if rootfs is on /dev/mapper/cryptroot
+    machine.wait_for_unit("multi-user.target")
+    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
+  '';
+})
diff --git a/nixos/tests/initrd-network-openvpn/default.nix b/nixos/tests/initrd-network-openvpn/default.nix
index bb4c41e6d70..769049905eb 100644
--- a/nixos/tests/initrd-network-openvpn/default.nix
+++ b/nixos/tests/initrd-network-openvpn/default.nix
@@ -1,3 +1,9 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+, systemdStage1 ? false
+}:
+
 import ../make-test-python.nix ({ lib, ...}:
 
 {
@@ -22,11 +28,12 @@ import ../make-test-python.nix ({ lib, ...}:
       minimalboot =
         { ... }:
         {
+          boot.initrd.systemd.enable = systemdStage1;
           boot.initrd.network = {
             enable = true;
             openvpn = {
               enable = true;
-              configuration = "/dev/null";
+              configuration = builtins.toFile "initrd.ovpn" "";
             };
           };
         };
@@ -39,6 +46,17 @@ import ../make-test-python.nix ({ lib, ...}:
           virtualisation.vlans = [ 1 ];
 
           boot.initrd = {
+            systemd.enable = systemdStage1;
+            systemd.extraBin.nc = "${pkgs.busybox}/bin/nc";
+            systemd.services.nc = {
+              requiredBy = ["initrd.target"];
+              after = ["network.target"];
+              serviceConfig = {
+                ExecStart = "/bin/nc -p 1234 -lke /bin/echo TESTVALUE";
+                Type = "oneshot";
+              };
+            };
+
             # This command does not fork to keep the VM in the state where
             # only the initramfs is loaded
             preLVMCommands =
@@ -91,6 +109,7 @@ import ../make-test-python.nix ({ lib, ...}:
             config = ''
               dev tun0
               ifconfig 10.8.0.1 10.8.0.2
+              cipher AES-256-CBC
               ${secretblock}
             '';
           };
diff --git a/nixos/tests/initrd-network-openvpn/initrd.ovpn b/nixos/tests/initrd-network-openvpn/initrd.ovpn
index 5926a48af00..3ada4130e86 100644
--- a/nixos/tests/initrd-network-openvpn/initrd.ovpn
+++ b/nixos/tests/initrd-network-openvpn/initrd.ovpn
@@ -3,6 +3,7 @@ dev tun
 ifconfig 10.8.0.2 10.8.0.1
 # Only force VLAN 2 through the VPN
 route 192.168.2.0 255.255.255.0 10.8.0.1
+cipher AES-256-CBC
 secret [inline]
 <secret>
 #
@@ -26,4 +27,4 @@ be5a69522a8e60ccb217f8521681b45d
 e7811584363597599cce2040a68ac00e
 f2125540e0f7f4adc37cb3f0d922eeb7
 -----END OpenVPN Static key V1-----
-</secret>
\ No newline at end of file
+</secret>
diff --git a/nixos/tests/initrd-network-ssh/default.nix b/nixos/tests/initrd-network-ssh/default.nix
index 0ad0563b0ce..017de688208 100644
--- a/nixos/tests/initrd-network-ssh/default.nix
+++ b/nixos/tests/initrd-network-ssh/default.nix
@@ -22,10 +22,6 @@ import ../make-test-python.nix ({ lib, ... }:
             hostKeys = [ ./ssh_host_ed25519_key ];
           };
         };
-        boot.initrd.extraUtilsCommands = ''
-          mkdir -p $out/secrets/etc/ssh
-          cat "${./ssh_host_ed25519_key}" > $out/secrets/etc/ssh/sh_host_ed25519_key
-        '';
         boot.initrd.preLVMCommands = ''
           while true; do
             if [ -f fnord ]; then
diff --git a/nixos/tests/initrd-secrets-changing.nix b/nixos/tests/initrd-secrets-changing.nix
new file mode 100644
index 00000000000..d6f9ef9ced8
--- /dev/null
+++ b/nixos/tests/initrd-secrets-changing.nix
@@ -0,0 +1,57 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+, lib ? pkgs.lib
+, testing ? import ../lib/testing-python.nix { inherit system pkgs; }
+}:
+
+let
+  secret1InStore = pkgs.writeText "topsecret" "iamasecret1";
+  secret2InStore = pkgs.writeText "topsecret" "iamasecret2";
+in
+
+testing.makeTest {
+  name = "initrd-secrets-changing";
+
+  nodes.machine = { ... }: {
+    virtualisation.useBootLoader = true;
+
+    boot.loader.grub.device = "/dev/vda";
+
+    boot.initrd.secrets = {
+      "/test" = secret1InStore;
+      "/run/keys/test" = secret1InStore;
+    };
+    boot.initrd.postMountCommands = "cp /test /mnt-root/secret-from-initramfs";
+
+    specialisation.secrets2System.configuration = {
+      boot.initrd.secrets = lib.mkForce {
+        "/test" = secret2InStore;
+        "/run/keys/test" = secret2InStore;
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    machine.wait_for_unit("multi-user.target")
+    print(machine.succeed("cat /run/keys/test"))
+    machine.succeed(
+        "cmp ${secret1InStore} /secret-from-initramfs",
+        "cmp ${secret1InStore} /run/keys/test",
+    )
+    # Select the second boot entry corresponding to the specialisation secrets2System.
+    machine.succeed("grub-reboot 1")
+    machine.shutdown()
+
+    with subtest("Check that the specialisation's secrets are distinct despite identical kernels"):
+        machine.wait_for_unit("multi-user.target")
+        print(machine.succeed("cat /run/keys/test"))
+        machine.succeed(
+            "cmp ${secret2InStore} /secret-from-initramfs",
+            "cmp ${secret2InStore} /run/keys/test",
+        )
+        machine.shutdown()
+  '';
+}
diff --git a/nixos/tests/installed-tests/fwupd.nix b/nixos/tests/installed-tests/fwupd.nix
index 65614e2689d..c095a50dc83 100644
--- a/nixos/tests/installed-tests/fwupd.nix
+++ b/nixos/tests/installed-tests/fwupd.nix
@@ -5,7 +5,7 @@ makeInstalledTest {
 
   testConfig = {
     services.fwupd.enable = true;
-    services.fwupd.disabledPlugins = lib.mkForce []; # don't disable test plugin
+    services.fwupd.daemonSettings.DisabledPlugins = lib.mkForce [ ]; # don't disable test plugin
     services.fwupd.enableTestRemote = true;
   };
 }
diff --git a/nixos/tests/installed-tests/pipewire.nix b/nixos/tests/installed-tests/pipewire.nix
index b04265658fc..6e69ada8612 100644
--- a/nixos/tests/installed-tests/pipewire.nix
+++ b/nixos/tests/installed-tests/pipewire.nix
@@ -1,15 +1,5 @@
-{ pkgs, lib, makeInstalledTest, ... }:
+{ pkgs, makeInstalledTest, ... }:
 
 makeInstalledTest {
   tested = pkgs.pipewire;
-  testConfig = {
-    hardware.pulseaudio.enable = false;
-    services.pipewire = {
-      enable = true;
-      pulse.enable = true;
-      jack.enable = true;
-      alsa.enable = true;
-      alsa.support32Bit = true;
-    };
-  };
 }
diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix
index 398ad8de19c..51d0d232ebb 100644
--- a/nixos/tests/installer.nix
+++ b/nixos/tests/installer.nix
@@ -21,6 +21,8 @@ let
             <nixpkgs/nixos/modules/testing/test-instrumentation.nix>
           ];
 
+        documentation.enable = false;
+
         # To ensure that we can rebuild the grub configuration on the nixos-rebuild
         system.extraDependencies = with pkgs; [ stdenvNoCC ];
 
@@ -49,6 +51,8 @@ let
           boot.loader.systemd-boot.enable = true;
         ''}
 
+        boot.initrd.secrets."/etc/secret" = ./secret;
+
         users.users.alice = {
           isNormalUser = true;
           home = "/home/alice";
@@ -73,9 +77,9 @@ let
     let iface = if grubVersion == 1 then "ide" else "virtio";
         isEfi = bootLoader == "systemd-boot" || (bootLoader == "grub" && grubUseEfi);
         bios  = if pkgs.stdenv.isAarch64 then "QEMU_EFI.fd" else "OVMF.fd";
-    in if !isEfi && !pkgs.stdenv.hostPlatform.isx86 then
-      throw "Non-EFI boot methods are only supported on i686 / x86_64"
-    else ''
+    in if !isEfi && !pkgs.stdenv.hostPlatform.isx86 then ''
+      machine.succeed("true")
+    '' else ''
       def assemble_qemu_flags():
           flags = "-cpu max"
           ${if (system == "x86_64-linux" || system == "i686-linux")
@@ -124,6 +128,7 @@ let
               }",
               "/mnt/etc/nixos/configuration.nix",
           )
+          machine.copy_from_host("${pkgs.writeText "secret" "secret"}", "/mnt/etc/nixos/secret")
 
       with subtest("Perform the installation"):
           machine.succeed("nixos-install < /dev/null >&2")
@@ -131,9 +136,21 @@ let
       with subtest("Do it again to make sure it's idempotent"):
           machine.succeed("nixos-install < /dev/null >&2")
 
+      with subtest("Check that we can build things in nixos-enter"):
+          machine.succeed(
+              """
+              nixos-enter -- nix-build --option substitute false -E 'derivation {
+                  name = "t";
+                  builder = "/bin/sh";
+                  args = ["-c" "echo nixos-enter build > $out"];
+                  system = builtins.currentSystem;
+                  preferLocalBuild = true;
+              }'
+              """
+          )
+
       with subtest("Shutdown system after installation"):
-          machine.succeed("umount /mnt/boot || true")
-          machine.succeed("umount /mnt")
+          machine.succeed("umount -R /mnt")
           machine.succeed("sync")
           machine.shutdown()
 
@@ -299,8 +316,9 @@ let
           # installer. This ensures the target disk (/dev/vda) is
           # the same during and after installation.
           virtualisation.emptyDiskImages = [ 512 ];
-          virtualisation.bootDevice =
+          virtualisation.rootDevice =
             if grubVersion == 1 then "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive2" else "/dev/vdb";
+          virtualisation.bootLoaderDevice = "/dev/vda";
           virtualisation.qemu.diskInterface =
             if grubVersion == 1 then "scsi" else "virtio";
 
@@ -327,6 +345,7 @@ let
             (docbook-xsl-ns.override {
               withManOptDedupPatch = true;
             })
+            kbd.dev
             kmod.dev
             libarchive.dev
             libxml2.bin
@@ -465,8 +484,12 @@ let
     '';
     testSpecialisationConfig = true;
   };
-
-
+  # disable zfs so we can support latest kernel if needed
+  no-zfs-module = {
+    nixpkgs.overlays = [(final: super: {
+      zfs = super.zfs.overrideAttrs(_: {meta.platforms = [];});}
+    )];
+  };
 in {
 
   # !!! `parted mkpart' seems to silently create overlapping partitions.
@@ -650,6 +673,55 @@ in {
     '';
   };
 
+  # Full disk encryption (root, kernel and initrd encrypted) using GRUB, GPT/UEFI,
+  # LVM-on-LUKS and a keyfile in initrd.secrets to enter the passphrase once
+  fullDiskEncryption = makeInstallerTest "fullDiskEncryption" {
+    createPartitions = ''
+      machine.succeed(
+          "flock /dev/vda parted --script /dev/vda -- mklabel gpt"
+          + " mkpart ESP fat32 1M 100MiB"  # /boot/efi
+          + " set 1 boot on"
+          + " mkpart primary ext2 1024MiB -1MiB",  # LUKS
+          "udevadm settle",
+          "modprobe dm_mod dm_crypt",
+          "dd if=/dev/random of=luks.key bs=256 count=1",
+          "echo -n supersecret | cryptsetup luksFormat -q --pbkdf-force-iterations 1000 --type luks1 /dev/vda2 -",
+          "echo -n supersecret | cryptsetup luksAddKey -q --pbkdf-force-iterations 1000 --key-file - /dev/vda2 luks.key",
+          "echo -n supersecret | cryptsetup luksOpen --key-file - /dev/vda2 crypt",
+          "pvcreate /dev/mapper/crypt",
+          "vgcreate crypt /dev/mapper/crypt",
+          "lvcreate -L 100M -n swap crypt",
+          "lvcreate -l '100%FREE' -n nixos crypt",
+          "mkfs.vfat -n efi /dev/vda1",
+          "mkfs.ext4 -L nixos /dev/crypt/nixos",
+          "mkswap -L swap /dev/crypt/swap",
+          "mount LABEL=nixos /mnt",
+          "mkdir -p /mnt/{etc/nixos,boot/efi}",
+          "mount LABEL=efi /mnt/boot/efi",
+          "swapon -L swap",
+          "mv luks.key /mnt/etc/nixos/"
+      )
+    '';
+    bootLoader = "grub";
+    grubUseEfi = true;
+    extraConfig = ''
+      boot.loader.grub.enableCryptodisk = true;
+      boot.loader.efi.efiSysMountPoint = "/boot/efi";
+
+      boot.initrd.secrets."/luks.key" = ./luks.key;
+      boot.initrd.luks.devices.crypt =
+        { device  = "/dev/vda2";
+          keyFile = "/luks.key";
+        };
+    '';
+    enableOCR = true;
+    preBootCommands = ''
+      machine.start()
+      machine.wait_for_text("Enter passphrase for")
+      machine.send_chars("supersecret\n")
+    '';
+  };
+
   swraid = makeInstallerTest "swraid" {
     createPartitions = ''
       machine.succeed(
@@ -714,6 +786,7 @@ in {
   bcachefsSimple = makeInstallerTest "bcachefs-simple" {
     extraInstallerConfig = {
       boot.supportedFilesystems = [ "bcachefs" ];
+      imports = [ no-zfs-module ];
     };
 
     createPartitions = ''
@@ -737,13 +810,22 @@ in {
   bcachefsEncrypted = makeInstallerTest "bcachefs-encrypted" {
     extraInstallerConfig = {
       boot.supportedFilesystems = [ "bcachefs" ];
+
+      # disable zfs so we can support latest kernel if needed
+      imports = [ no-zfs-module ];
+
       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";
+      boot.kernelParams = lib.mkAfter [ "console=tty0" ];
+    '';
+
+    enableOCR = true;
+    preBootCommands = ''
+      machine.start()
+      machine.wait_for_text("enter passphrase for ")
+      machine.send_chars("password\n")
     '';
 
     createPartitions = ''
@@ -769,6 +851,9 @@ in {
   bcachefsMulti = makeInstallerTest "bcachefs-multi" {
     extraInstallerConfig = {
       boot.supportedFilesystems = [ "bcachefs" ];
+
+      # disable zfs so we can support latest kernel if needed
+      imports = [ no-zfs-module ];
     };
 
     createPartitions = ''
diff --git a/nixos/tests/isso.nix b/nixos/tests/isso.nix
index 575e1c52ecc..4ec8b5ec359 100644
--- a/nixos/tests/isso.nix
+++ b/nixos/tests/isso.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "isso";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ asbachb ];
+    maintainers = [ ];
   };
 
   nodes.machine = { config, pkgs, ... }: {
diff --git a/nixos/tests/k3s/default.nix b/nixos/tests/k3s/default.nix
index 07d93c41c7a..e168f8233c7 100644
--- a/nixos/tests/k3s/default.nix
+++ b/nixos/tests/k3s/default.nix
@@ -1,9 +1,13 @@
 { system ? builtins.currentSystem
 , pkgs ? import ../../.. { inherit system; }
+, lib ? pkgs.lib
 }:
+let
+  allK3s = lib.filterAttrs (n: _: lib.strings.hasPrefix "k3s_" n) pkgs;
+in
 {
   # Run a single node k3s cluster and verify a pod can run
-  single-node = import ./single-node.nix { inherit system pkgs; };
+  single-node = lib.mapAttrs (_: k3s: import ./single-node.nix { inherit system pkgs k3s; }) allK3s;
   # Run a multi-node k3s cluster and verify pod networking works across nodes
-  multi-node = import ./multi-node.nix { inherit system pkgs; };
+  multi-node = lib.mapAttrs (_: k3s: import ./multi-node.nix { inherit system pkgs k3s; }) allK3s;
 }
diff --git a/nixos/tests/k3s/multi-node.nix b/nixos/tests/k3s/multi-node.nix
index 9a6c7fd4657..932b4639b39 100644
--- a/nixos/tests/k3s/multi-node.nix
+++ b/nixos/tests/k3s/multi-node.nix
@@ -1,4 +1,4 @@
-import ../make-test-python.nix ({ pkgs, lib, ... }:
+import ../make-test-python.nix ({ pkgs, lib, k3s, ... }:
   let
     imageEnv = pkgs.buildEnv {
       name = "k3s-pause-image-env";
@@ -39,7 +39,7 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
     tokenFile = pkgs.writeText "token" "p@s$w0rd";
   in
   {
-    name = "k3s-multi-node";
+    name = "${k3s.name}-multi-node";
 
     nodes = {
       server = { pkgs, ... }: {
@@ -52,7 +52,7 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
           inherit tokenFile;
           enable = true;
           role = "server";
-          package = pkgs.k3s;
+          package = k3s;
           clusterInit = true;
           extraFlags = builtins.toString [
             "--disable" "coredns"
diff --git a/nixos/tests/k3s/single-node.nix b/nixos/tests/k3s/single-node.nix
index a95fa4a031e..d61595d889e 100644
--- a/nixos/tests/k3s/single-node.nix
+++ b/nixos/tests/k3s/single-node.nix
@@ -1,4 +1,4 @@
-import ../make-test-python.nix ({ pkgs, lib, ... }:
+import ../make-test-python.nix ({ pkgs, lib, k3s, ... }:
   let
     imageEnv = pkgs.buildEnv {
       name = "k3s-pause-image-env";
@@ -24,7 +24,7 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
     '';
   in
   {
-    name = "k3s";
+    name = "${k3s.name}-single-node";
     meta = with pkgs.lib.maintainers; {
       maintainers = [ euank ];
     };
@@ -38,7 +38,7 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
 
       services.k3s.enable = true;
       services.k3s.role = "server";
-      services.k3s.package = pkgs.k3s;
+      services.k3s.package = k3s;
       # Slightly reduce resource usage
       services.k3s.extraFlags = builtins.toString [
         "--disable" "coredns"
@@ -77,6 +77,9 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
       machine.succeed("k3s kubectl wait --for 'condition=Ready' pod/test")
       machine.succeed("k3s kubectl delete -f ${testPodYaml}")
 
+      # regression test for #176445
+      machine.fail("journalctl -o cat -u k3s.service | grep 'ipset utility not found'")
+
       machine.shutdown()
     '';
   })
diff --git a/nixos/tests/kanidm.nix b/nixos/tests/kanidm.nix
index 33c65026b9b..d9c0542c4c2 100644
--- a/nixos/tests/kanidm.nix
+++ b/nixos/tests/kanidm.nix
@@ -44,7 +44,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
         };
       };
 
-      networking.hosts."${nodes.server.config.networking.primaryIPAddress}" = [ serverDomain ];
+      networking.hosts."${nodes.server.networking.primaryIPAddress}" = [ serverDomain ];
 
       security.pki.certificateFiles = [ certs.ca.cert ];
     };
@@ -56,7 +56,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
         # 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;
+          nodes.server.services.kanidm.serverSettings;
         serverConfigFile = (pkgs.formats.toml { }).generate "server.toml" filteredConfig;
 
       in
diff --git a/nixos/tests/kavita.nix b/nixos/tests/kavita.nix
new file mode 100644
index 00000000000..f27b3fffbcf
--- /dev/null
+++ b/nixos/tests/kavita.nix
@@ -0,0 +1,36 @@
+import ./make-test-python.nix ({ pkgs, ...} : {
+  name = "kavita";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ misterio77 ];
+  };
+
+  nodes = {
+    kavita = { config, pkgs, ... }: {
+      services.kavita = {
+        enable = true;
+        port = 5000;
+        tokenKeyFile = builtins.toFile "kavita.key" "QfpjFvjT83BLtZ74GE3U3Q==";
+      };
+    };
+  };
+
+  testScript = let
+    regUrl = "http://kavita:5000/api/Account/register";
+    payload = builtins.toFile "payload.json" (builtins.toJSON {
+      username = "foo";
+      password = "correcthorsebatterystaple";
+      email = "foo@bar";
+    });
+  in ''
+    kavita.start
+    kavita.wait_for_unit("kavita.service")
+
+    # Check that static assets are working
+    kavita.wait_until_succeeds("curl http://kavita:5000/site.webmanifest | grep Kavita")
+
+    # Check that registration is working
+    kavita.succeed("curl -fX POST ${regUrl} --json @${payload}")
+    # But only for the first one
+    kavita.fail("curl -fX POST ${regUrl} --json @${payload}")
+  '';
+})
diff --git a/nixos/tests/kea.nix b/nixos/tests/kea.nix
index b1d5894cc7c..b4095893b48 100644
--- a/nixos/tests/kea.nix
+++ b/nixos/tests/kea.nix
@@ -1,3 +1,10 @@
+# This test verifies DHCPv4 interaction between a client and a router.
+# For successful DHCP allocations a dynamic update request is sent
+# towards a nameserver to allocate a name in the lan.nixos.test zone.
+# We then verify whether client and router can ping each other, and
+# that the nameserver can resolve the clients fqdn to the correct IP
+# address.
+
 import ./make-test-python.nix ({ pkgs, lib, ...}: {
   meta.maintainers = with lib.maintainers; [ hexa ];
 
@@ -8,17 +15,17 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: {
       virtualisation.vlans = [ 1 ];
 
       networking = {
-        useNetworkd = true;
         useDHCP = false;
         firewall.allowedUDPPorts = [ 67 ];
       };
 
       systemd.network = {
+        enable = true;
         networks = {
           "01-eth1" = {
             name = "eth1";
             networkConfig = {
-              Address = "10.0.0.1/30";
+              Address = "10.0.0.1/29";
             };
           };
         };
@@ -45,13 +52,115 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: {
           };
 
           subnet4 = [ {
-            subnet = "10.0.0.0/30";
+            subnet = "10.0.0.0/29";
             pools = [ {
-              pool = "10.0.0.2 - 10.0.0.2";
+              pool = "10.0.0.3 - 10.0.0.3";
             } ];
           } ];
+
+          # Enable communication between dhcp4 and a local dhcp-ddns
+          # instance.
+          # https://kea.readthedocs.io/en/kea-2.2.0/arm/dhcp4-srv.html#ddns-for-dhcpv4
+          dhcp-ddns = {
+            enable-updates = true;
+          };
+
+          ddns-send-updates = true;
+          ddns-qualifying-suffix = "lan.nixos.test.";
         };
       };
+
+      services.kea.dhcp-ddns = {
+        enable = true;
+        settings = {
+          forward-ddns = {
+            # Configure updates of a forward zone named `lan.nixos.test`
+            # hosted at the nameserver at 10.0.0.2
+            # https://kea.readthedocs.io/en/kea-2.2.0/arm/ddns.html#adding-forward-dns-servers
+            ddns-domains = [ {
+              name = "lan.nixos.test.";
+              # Use a TSIG key in production!
+              key-name = "";
+              dns-servers = [ {
+                ip-address = "10.0.0.2";
+                port = 53;
+              } ];
+            } ];
+          };
+        };
+      };
+    };
+
+    nameserver = { config, pkgs, ... }: {
+      virtualisation.vlans = [ 1 ];
+
+      networking = {
+        useDHCP = false;
+        firewall.allowedUDPPorts = [ 53 ];
+      };
+
+      systemd.network = {
+        enable = true;
+        networks = {
+          "01-eth1" = {
+            name = "eth1";
+            networkConfig = {
+              Address = "10.0.0.2/29";
+            };
+          };
+        };
+      };
+
+      services.resolved.enable = false;
+
+      # Set up an authoritative nameserver, serving the `lan.nixos.test`
+      # zone and configure an ACL that allows dynamic updates from
+      # the router's ip address.
+      # This ACL is likely insufficient for production usage. Please
+      # use TSIG keys.
+      services.knot = let
+        zone = pkgs.writeTextDir "lan.nixos.test.zone" ''
+          @ SOA ns.nixos.test nox.nixos.test 0 86400 7200 3600000 172800
+          @ NS nameserver
+          nameserver A 10.0.0.3
+          router A 10.0.0.1
+        '';
+        zonesDir = pkgs.buildEnv {
+          name = "knot-zones";
+          paths = [ zone ];
+        };
+      in {
+        enable = true;
+        extraArgs = [
+          "-v"
+        ];
+        extraConfig = ''
+          server:
+              listen: 0.0.0.0@53
+
+          log:
+            - target: syslog
+              any: debug
+
+          acl:
+            - id: dhcp_ddns
+              address: 10.0.0.1
+              action: update
+
+          template:
+            - id: default
+              storage: ${zonesDir}
+              zonefile-sync: -1
+              zonefile-load: difference-no-serial
+              journal-content: all
+
+          zone:
+            - domain: lan.nixos.test
+              file: lan.nixos.test.zone
+              acl: [dhcp_ddns]
+        '';
+      };
+
     };
 
     client = { config, pkgs, ... }: {
@@ -70,6 +179,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: {
     router.wait_for_unit("kea-dhcp4-server.service")
     client.wait_for_unit("systemd-networkd-wait-online.service")
     client.wait_until_succeeds("ping -c 5 10.0.0.1")
-    router.wait_until_succeeds("ping -c 5 10.0.0.2")
+    router.wait_until_succeeds("ping -c 5 10.0.0.3")
+    nameserver.wait_until_succeeds("kdig +short client.lan.nixos.test @10.0.0.2 | grep -q 10.0.0.3")
   '';
 })
diff --git a/nixos/tests/keepassxc.nix b/nixos/tests/keepassxc.nix
index 303be133040..a4f452412cd 100644
--- a/nixos/tests/keepassxc.nix
+++ b/nixos/tests/keepassxc.nix
@@ -4,6 +4,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
   name = "keepassxc";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ turion ];
+    timeout = 1800;
   };
 
   nodes.machine = { ... }:
@@ -17,7 +18,7 @@ import ./make-test-python.nix ({ pkgs, ...} :
     services.xserver.enable = true;
 
     # Regression test for https://github.com/NixOS/nixpkgs/issues/163482
-    qt5 = {
+    qt = {
       enable = true;
       platformTheme = "gnome";
       style = "adwaita-dark";
@@ -55,9 +56,12 @@ import ./make-test-python.nix ({ pkgs, ...} :
         machine.sleep(5)
         # Regression #163482: keepassxc did not crash
         machine.succeed("ps -e | grep keepassxc")
-        machine.wait_for_text("foo.kdbx")
+        machine.wait_for_text("Open database")
         machine.send_key("ret")
-        machine.sleep(1)
+
+        # Wait for the enter password screen to appear.
+        machine.wait_for_text("/home/alice/foo.kdbx")
+
         # Click on "Browse" button to select keyfile
         machine.send_key("tab")
         machine.send_chars("/home/alice/foo.keyfile")
diff --git a/nixos/tests/kernel-generic.nix b/nixos/tests/kernel-generic.nix
index 7ee734a1eff..3e74554de33 100644
--- a/nixos/tests/kernel-generic.nix
+++ b/nixos/tests/kernel-generic.nix
@@ -30,7 +30,7 @@ let
       linux_5_4_hardened
       linux_5_10_hardened
       linux_5_15_hardened
-      linux_6_0_hardened
+      linux_6_1_hardened
 
       linux_testing;
   };
diff --git a/nixos/tests/keyd.nix b/nixos/tests/keyd.nix
new file mode 100644
index 00000000000..d492cc19489
--- /dev/null
+++ b/nixos/tests/keyd.nix
@@ -0,0 +1,82 @@
+# The test template is taken from the `./keymap.nix`
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+let
+  readyFile = "/tmp/readerReady";
+  resultFile = "/tmp/readerResult";
+
+  testReader = pkgs.writeScript "test-input-reader" ''
+    rm -f ${resultFile} ${resultFile}.tmp
+    logger "testReader: START: Waiting for $1 characters, expecting '$2'."
+    touch ${readyFile}
+    read -r -N $1 chars
+    rm -f ${readyFile}
+    if [ "$chars" == "$2" ]; then
+      logger -s "testReader: PASS: Got '$2' as expected." 2>${resultFile}.tmp
+    else
+      logger -s "testReader: FAIL: Expected '$2' but got '$chars'." 2>${resultFile}.tmp
+    fi
+    # rename after the file is written to prevent a race condition
+    mv  ${resultFile}.tmp ${resultFile}
+  '';
+
+
+  mkKeyboardTest = name: { settings, test }: with pkgs.lib; makeTest {
+    inherit name;
+
+    nodes.machine = {
+      services.keyd = {
+        enable = true;
+        inherit settings;
+      };
+    };
+
+    testScript = ''
+      import shlex
+
+      machine.wait_for_unit("keyd.service")
+
+      def run_test_case(cmd, test_case_name, inputs, expected):
+          with subtest(test_case_name):
+              assert len(inputs) == len(expected)
+              machine.execute("rm -f ${readyFile} ${resultFile}")
+              # set up process that expects all the keys to be entered
+              machine.succeed(
+                  "{} {} {} {} >&2 &".format(
+                      cmd,
+                      "${testReader}",
+                      len(inputs),
+                      shlex.quote("".join(expected)),
+                  )
+              )
+              # wait for reader to be ready
+              machine.wait_for_file("${readyFile}")
+              # send all keys
+              for key in inputs:
+                  machine.send_key(key)
+              # wait for result and check
+              machine.wait_for_file("${resultFile}")
+              machine.succeed("grep -q 'PASS:' ${resultFile}")
+      test = ${builtins.toJSON test}
+      run_test_case("openvt -sw --", "${name}", test["press"], test["expect"])
+    '';
+  };
+
+in
+pkgs.lib.mapAttrs mkKeyboardTest {
+  swap-ab_and_ctrl-as-shift = {
+    test.press = [ "a" "ctrl-b" "c" ];
+    test.expect = [ "b" "A" "c" ];
+
+    settings.main = {
+      "a" = "b";
+      "b" = "a";
+      "control" = "oneshot(shift)";
+    };
+  };
+}
diff --git a/nixos/tests/keymap.nix b/nixos/tests/keymap.nix
index 4306a9ae2cf..0bde21093b0 100644
--- a/nixos/tests/keymap.nix
+++ b/nixos/tests/keymap.nix
@@ -64,7 +64,6 @@ let
 
               # wait for reader to be ready
               machine.wait_for_file("${readyFile}")
-              machine.sleep(1)
 
               # send all keys
               for key in inputs:
@@ -78,9 +77,18 @@ let
       with open("${pkgs.writeText "tests.json" (builtins.toJSON tests)}") as json_file:
           tests = json.load(json_file)
 
+      # These environments used to run in the opposite order, causing the
+      # following error at openvt startup.
+      #
+      # openvt: Couldn't deallocate console 1
+      #
+      # This error did not appear in successful runs.
+      # I don't know the exact cause, but I it seems that openvt and X are
+      # fighting over the virtual terminal. This does not appear to be a problem
+      # when the X test runs first.
       keymap_environments = {
-          "VT Keymap": "openvt -sw --",
           "Xorg Keymap": "DISPLAY=:0 xterm -title testterm -class testterm -fullscreen -e",
+          "VT Keymap": "openvt -sw --",
       }
 
       machine.wait_for_x()
diff --git a/nixos/tests/knot.nix b/nixos/tests/knot.nix
index 203fd03fac2..2ecbf69194b 100644
--- a/nixos/tests/knot.nix
+++ b/nixos/tests/knot.nix
@@ -31,7 +31,7 @@ let
   # DO NOT USE pkgs.writeText IN PRODUCTION. This put secrets in the nix store!
   tsigFile = pkgs.writeText "tsig.conf" ''
     key:
-      - id: slave_key
+      - id: xfr_key
         algorithm: hmac-sha256
         secret: zOYgOgnzx3TGe5J5I/0kxd7gTcxXhLYMEq3Ek3fY37s=
   '';
@@ -43,7 +43,7 @@ in {
 
 
   nodes = {
-    master = { lib, ... }: {
+    primary = { lib, ... }: {
       imports = [ common ];
 
       # trigger sched_setaffinity syscall
@@ -64,22 +64,17 @@ in {
         server:
             listen: 0.0.0.0@53
             listen: ::@53
-
-        acl:
-          - id: slave_acl
-            address: 192.168.0.2
-            key: slave_key
-            action: transfer
+            automatic-acl: true
 
         remote:
-          - id: slave
+          - id: secondary
             address: 192.168.0.2@53
+            key: xfr_key
 
         template:
           - id: default
             storage: ${knotZonesEnv}
-            notify: [slave]
-            acl: [slave_acl]
+            notify: [secondary]
             dnssec-signing: on
             # Input-only zone files
             # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-3
@@ -105,7 +100,7 @@ in {
       '';
     };
 
-    slave = { lib, ... }: {
+    secondary = { lib, ... }: {
       imports = [ common ];
       networking.interfaces.eth1 = {
         ipv4.addresses = lib.mkForce [
@@ -122,21 +117,16 @@ in {
         server:
             listen: 0.0.0.0@53
             listen: ::@53
-
-        acl:
-          - id: notify_from_master
-            address: 192.168.0.1
-            action: notify
+            automatic-acl: true
 
         remote:
-          - id: master
+          - id: primary
             address: 192.168.0.1@53
-            key: slave_key
+            key: xfr_key
 
         template:
           - id: default
-            master: master
-            acl: [notify_from_master]
+            master: primary
             # zonefileless setup
             # https://www.knot-dns.cz/docs/2.8/html/operation.html#example-2
             zonefile-sync: -1
@@ -174,19 +164,19 @@ in {
   };
 
   testScript = { nodes, ... }: let
-    master4 = (lib.head nodes.master.config.networking.interfaces.eth1.ipv4.addresses).address;
-    master6 = (lib.head nodes.master.config.networking.interfaces.eth1.ipv6.addresses).address;
+    primary4 = (lib.head nodes.primary.config.networking.interfaces.eth1.ipv4.addresses).address;
+    primary6 = (lib.head nodes.primary.config.networking.interfaces.eth1.ipv6.addresses).address;
 
-    slave4 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv4.addresses).address;
-    slave6 = (lib.head nodes.slave.config.networking.interfaces.eth1.ipv6.addresses).address;
+    secondary4 = (lib.head nodes.secondary.config.networking.interfaces.eth1.ipv4.addresses).address;
+    secondary6 = (lib.head nodes.secondary.config.networking.interfaces.eth1.ipv6.addresses).address;
   in ''
     import re
 
     start_all()
 
     client.wait_for_unit("network.target")
-    master.wait_for_unit("knot.service")
-    slave.wait_for_unit("knot.service")
+    primary.wait_for_unit("knot.service")
+    secondary.wait_for_unit("knot.service")
 
 
     def test(host, query_type, query, pattern):
@@ -195,7 +185,7 @@ in {
         assert re.search(pattern, out), f'Did not match "{pattern}"'
 
 
-    for host in ("${master4}", "${master6}", "${slave4}", "${slave6}"):
+    for host in ("${primary4}", "${primary6}", "${secondary4}", "${secondary6}"):
         with subtest(f"Interrogate {host}"):
             test(host, "SOA", "example.com", r"start of authority.*noc\.example\.com\.")
             test(host, "A", "example.com", r"has no [^ ]+ record")
@@ -211,6 +201,6 @@ in {
             test(host, "RRSIG", "www.example.com", r"RR set signature is")
             test(host, "DNSKEY", "example.com", r"DNSSEC key is")
 
-    master.log(master.succeed("systemd-analyze security knot.service | grep -v '✓'"))
+    primary.log(primary.succeed("systemd-analyze security knot.service | grep -v '✓'"))
   '';
 })
diff --git a/nixos/tests/kubo.nix b/nixos/tests/kubo.nix
index 94aa24a9204..496f409a40a 100644
--- a/nixos/tests/kubo.nix
+++ b/nixos/tests/kubo.nix
@@ -1,10 +1,10 @@
-import ./make-test-python.nix ({ pkgs, ...} : {
+{ lib, ...} : {
   name = "kubo";
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ mguentner ];
+  meta = with lib.maintainers; {
+    maintainers = [ mguentner Luflosi ];
   };
 
-  nodes.machine = { ... }: {
+  nodes.machine = { config, ... }: {
     services.kubo = {
       enable = true;
       # Also will add a unix domain socket socket API address, see module.
@@ -12,50 +12,74 @@ import ./make-test-python.nix ({ pkgs, ...} : {
       settings.Addresses.API = "/ip4/127.0.0.1/tcp/2324";
       dataDir = "/mnt/ipfs";
     };
+    users.users.alice = {
+      isNormalUser = true;
+      extraGroups = [ config.services.kubo.group ];
+    };
   };
 
-  nodes.fuse = { ... }: {
+  nodes.fuse = { config, ... }: {
     services.kubo = {
       enable = true;
-      settings.Addresses.API = "/ip4/127.0.0.1/tcp/2324";
       autoMount = true;
     };
+    users.users.alice = {
+      isNormalUser = true;
+      extraGroups = [ config.services.kubo.group ];
+    };
+    users.users.bob = {
+      isNormalUser = true;
+    };
   };
 
   testScript = ''
     start_all()
 
-    # IPv4 activation
-
-    machine.succeed("ipfs --api /ip4/127.0.0.1/tcp/2324 id")
-    ipfs_hash = machine.succeed(
-        "echo fnord | ipfs --api /ip4/127.0.0.1/tcp/2324 add | awk '{ print $2 }'"
-    )
+    with subtest("Automatic socket activation"):
+        ipfs_hash = machine.succeed(
+            "echo fnord0 | su alice -l -c 'ipfs add --quieter'"
+        )
+        machine.succeed(f"ipfs cat /ipfs/{ipfs_hash.strip()} | grep fnord0")
 
-    machine.succeed(f"ipfs cat /ipfs/{ipfs_hash.strip()} | grep fnord")
+    machine.stop_job("ipfs")
 
-    # Unix domain socket activation
+    with subtest("IPv4 socket activation"):
+        machine.succeed("ipfs --api /ip4/127.0.0.1/tcp/2324 id")
+        ipfs_hash = machine.succeed(
+            "echo fnord | ipfs --api /ip4/127.0.0.1/tcp/2324 add --quieter"
+        )
+        machine.succeed(f"ipfs cat /ipfs/{ipfs_hash.strip()} | grep fnord")
 
     machine.stop_job("ipfs")
 
-    ipfs_hash = machine.succeed(
-        "echo fnord2 | ipfs --api /unix/run/ipfs.sock add | awk '{ print $2 }'"
-    )
-    machine.succeed(
-        f"ipfs --api /unix/run/ipfs.sock cat /ipfs/{ipfs_hash.strip()} | grep fnord2"
-    )
-
-    # 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")
+    with subtest("Unix domain socket activation"):
+        ipfs_hash = machine.succeed(
+            "echo fnord2 | ipfs --api /unix/run/ipfs.sock add --quieter"
+        )
+        machine.succeed(
+            f"ipfs --api /unix/run/ipfs.sock cat /ipfs/{ipfs_hash.strip()} | grep fnord2"
+        )
+
+    with subtest("Setting dataDir works properly with the hardened systemd unit"):
+        machine.succeed("test -e /mnt/ipfs/config")
+        machine.succeed("test ! -e /var/lib/ipfs/")
+
+    with subtest("FUSE mountpoint"):
+        fuse.fail("echo a | su bob -l -c 'ipfs add --quieter'")
+        # The FUSE mount functionality is broken as of v0.13.0 and v0.17.0.
+        # See https://github.com/ipfs/kubo/issues/9044.
+        # Workaround: using CID Version 1 avoids that.
+        ipfs_hash = fuse.succeed(
+            "echo fnord3 | su alice -l -c 'ipfs add --quieter --cid-version=1'"
+        ).strip()
+
+        fuse.succeed(f"cat /ipfs/{ipfs_hash} | grep fnord3")
+
+    with subtest("Unmounting of /ipns and /ipfs"):
+        # Force Kubo to crash and wait for it to restart
+        fuse.systemctl("kill --signal=SIGKILL ipfs.service")
+        fuse.wait_for_unit("ipfs.service", timeout = 30)
+
+        fuse.succeed(f"cat /ipfs/{ipfs_hash} | grep fnord3")
   '';
-})
+}
diff --git a/nixos/tests/libreswan.nix b/nixos/tests/libreswan.nix
index ff3d2344a67..aadba941fab 100644
--- a/nixos/tests/libreswan.nix
+++ b/nixos/tests/libreswan.nix
@@ -107,6 +107,8 @@ in
 
       with subtest("Network is up"):
           alice.wait_until_succeeds("ping -c1 bob")
+          alice.succeed("systemctl restart ipsec")
+          bob.succeed("systemctl restart ipsec")
 
       with subtest("Eve can eavesdrop cleartext traffic"):
           eavesdrop()
diff --git a/nixos/tests/libvirtd.nix b/nixos/tests/libvirtd.nix
index 49258fcb93e..b6e60407599 100644
--- a/nixos/tests/libvirtd.nix
+++ b/nixos/tests/libvirtd.nix
@@ -21,7 +21,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   };
 
   testScript = let
-    nixosInstallISO = (import ../release.nix {}).iso_minimal.${pkgs.hostPlatform.system};
+    nixosInstallISO = (import ../release.nix {}).iso_minimal.${pkgs.stdenv.hostPlatform.system};
     virshShutdownCmd = if pkgs.stdenv.isx86_64 then "shutdown" else "destroy";
   in ''
     start_all()
diff --git a/nixos/tests/lldap.nix b/nixos/tests/lldap.nix
new file mode 100644
index 00000000000..d6c3a865aa0
--- /dev/null
+++ b/nixos/tests/lldap.nix
@@ -0,0 +1,26 @@
+import ./make-test-python.nix ({ ... }: {
+  name = "lldap";
+
+  nodes.machine = { pkgs, ... }: {
+    services.lldap = {
+      enable = true;
+      settings = {
+        verbose = true;
+        ldap_base_dn = "dc=example,dc=com";
+      };
+    };
+    environment.systemPackages = [ pkgs.openldap ];
+  };
+
+  testScript = ''
+    machine.wait_for_unit("lldap.service")
+    machine.wait_for_open_port(3890)
+    machine.wait_for_open_port(17170)
+
+    machine.succeed("curl --location --fail http://localhost:17170/")
+
+    print(
+      machine.succeed('ldapsearch -H ldap://localhost:3890 -D uid=admin,ou=people,dc=example,dc=com -b "ou=people,dc=example,dc=com" -w password')
+    )
+  '';
+})
diff --git a/nixos/tests/login.nix b/nixos/tests/login.nix
index 2cff38d2005..67f5764a0a1 100644
--- a/nixos/tests/login.nix
+++ b/nixos/tests/login.nix
@@ -13,6 +13,8 @@ import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
     };
 
   testScript = ''
+      machine.start(allow_reboot = True)
+
       machine.wait_for_unit("multi-user.target")
       machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
       machine.screenshot("postboot")
@@ -53,7 +55,14 @@ import ./make-test-python.nix ({ pkgs, latestKernel ? false, ... }:
           machine.screenshot("getty")
 
       with subtest("Check whether ctrl-alt-delete works"):
-          machine.send_key("ctrl-alt-delete")
-          machine.wait_for_shutdown()
+          boot_id1 = machine.succeed("cat /proc/sys/kernel/random/boot_id").strip()
+          assert boot_id1 != ""
+
+          machine.reboot()
+
+          boot_id2 = machine.succeed("cat /proc/sys/kernel/random/boot_id").strip()
+          assert boot_id2 != ""
+
+          assert boot_id1 != boot_id2
   '';
 })
diff --git a/nixos/tests/luks.nix b/nixos/tests/luks.nix
new file mode 100644
index 00000000000..c2b95c6a95f
--- /dev/null
+++ b/nixos/tests/luks.nix
@@ -0,0 +1,69 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "luks";
+
+  nodes.machine = { pkgs, ... }: {
+    # Use systemd-boot
+    virtualisation = {
+      emptyDiskImages = [ 512 512 ];
+      useBootLoader = true;
+      useEFIBoot = true;
+    };
+    boot.loader.systemd-boot.enable = true;
+
+    boot.kernelParams = lib.mkOverride 5 [ "console=tty1" ];
+
+    environment.systemPackages = with pkgs; [ cryptsetup ];
+
+    specialisation = rec {
+      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/vdb";
+          cryptroot2.device = "/dev/vdc";
+        };
+        virtualisation.rootDevice = "/dev/mapper/cryptroot";
+      };
+      boot-luks-custom-keymap.configuration = lib.mkMerge [
+        boot-luks.configuration
+        {
+          console.keyMap = "neo";
+        }
+      ];
+    };
+  };
+
+  enableOCR = true;
+
+  testScript = ''
+    # Create encrypted volume
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdb -")
+    machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /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.start()
+    machine.wait_for_text("Passphrase for")
+    machine.send_chars("supersecret\n")
+    machine.wait_for_unit("multi-user.target")
+
+    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
+
+    # Boot from the encrypted disk with custom keymap
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks-custom-keymap.conf")
+    machine.succeed("sync")
+    machine.crash()
+
+    # Boot and decrypt the disk
+    machine.start()
+    machine.wait_for_text("Passphrase for")
+    machine.send_chars("havfkhfrkfl\n")
+    machine.wait_for_unit("multi-user.target")
+
+    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
+  '';
+})
diff --git a/nixos/tests/lvm2/systemd-stage-1.nix b/nixos/tests/lvm2/systemd-stage-1.nix
index 617ba77b179..b711cd22d7f 100644
--- a/nixos/tests/lvm2/systemd-stage-1.nix
+++ b/nixos/tests/lvm2/systemd-stage-1.nix
@@ -1,18 +1,18 @@
 { kernelPackages ? null, flavour }: let
   preparationCode = {
     raid = ''
-      machine.succeed("vgcreate test_vg /dev/vdc /dev/vdd")
+      machine.succeed("vgcreate test_vg /dev/vdb /dev/vdc")
       machine.succeed("lvcreate -L 512M --type raid0 test_vg -n test_lv")
     '';
 
     thinpool = ''
-      machine.succeed("vgcreate test_vg /dev/vdc")
+      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")
     '';
 
     vdo = ''
-      machine.succeed("vgcreate test_vg /dev/vdc")
+      machine.succeed("vgcreate test_vg /dev/vdb")
       machine.succeed("lvcreate --type vdo -n test_lv -L 6G -V 12G test_vg/vdo_pool_lv")
     '';
   }.${flavour};
@@ -79,7 +79,7 @@ in import ../make-test-python.nix ({ pkgs, ... }: {
       kernelPackages = lib.mkIf (kernelPackages != null) kernelPackages;
     };
 
-    specialisation.boot-lvm.configuration.virtualisation.bootDevice = "/dev/test_vg/test_lv";
+    specialisation.boot-lvm.configuration.virtualisation.rootDevice = "/dev/test_vg/test_lv";
   };
 
   testScript = ''
diff --git a/nixos/tests/maddy/default.nix b/nixos/tests/maddy/default.nix
new file mode 100644
index 00000000000..043906863e6
--- /dev/null
+++ b/nixos/tests/maddy/default.nix
@@ -0,0 +1,6 @@
+{ handleTest }:
+
+{
+  unencrypted = handleTest ./unencrypted.nix { };
+  tls = handleTest ./tls.nix { };
+}
diff --git a/nixos/tests/maddy/tls.nix b/nixos/tests/maddy/tls.nix
new file mode 100644
index 00000000000..44da4cf2a3c
--- /dev/null
+++ b/nixos/tests/maddy/tls.nix
@@ -0,0 +1,94 @@
+import ../make-test-python.nix ({ pkgs, ... }:
+let
+  certs = import ../common/acme/server/snakeoil-certs.nix;
+  domain = certs.domain;
+in {
+  name = "maddy-tls";
+  meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; };
+
+  nodes = {
+    server = { options, ... }: {
+      services.maddy = {
+        enable = true;
+        hostname = domain;
+        primaryDomain = domain;
+        openFirewall = true;
+        ensureAccounts = [ "postmaster@${domain}" ];
+        ensureCredentials = {
+          # Do not use this in production. This will make passwords world-readable
+          # in the Nix store
+          "postmaster@${domain}".passwordFile = "${pkgs.writeText "postmaster" "test"}";
+        };
+        tls = {
+          loader = "file";
+          certificates = [{
+            certPath = "${certs.${domain}.cert}";
+            keyPath = "${certs.${domain}.key}";
+          }];
+        };
+        # Enable TLS listeners. Configuring this via the module is not yet
+        # implemented.
+        config = builtins.replaceStrings [
+          "imap tcp://0.0.0.0:143"
+          "submission tcp://0.0.0.0:587"
+        ] [
+          "imap tls://0.0.0.0:993 tcp://0.0.0.0:143"
+          "submission tls://0.0.0.0:465 tcp://0.0.0.0:587"
+        ] options.services.maddy.config.default;
+      };
+      # Not covered by openFirewall yet
+      networking.firewall.allowedTCPPorts = [ 993 465 ];
+    };
+
+    client = { nodes, ... }: {
+      security.pki.certificateFiles = [
+        certs.ca.cert
+      ];
+      networking.extraHosts = ''
+        ${nodes.server.networking.primaryIPAddress} ${domain}
+     '';
+      environment.systemPackages = [
+        (pkgs.writers.writePython3Bin "send-testmail" { } ''
+          import smtplib
+          import ssl
+          from email.mime.text import MIMEText
+
+          context = ssl.create_default_context()
+          msg = MIMEText("Hello World")
+          msg['Subject'] = 'Test'
+          msg['From'] = "postmaster@${domain}"
+          msg['To'] = "postmaster@${domain}"
+          with smtplib.SMTP_SSL(host='${domain}', port=465, context=context) as smtp:
+              smtp.login('postmaster@${domain}', 'test')
+              smtp.sendmail(
+                'postmaster@${domain}', 'postmaster@${domain}', msg.as_string()
+              )
+        '')
+        (pkgs.writers.writePython3Bin "test-imap" { } ''
+          import imaplib
+
+          with imaplib.IMAP4_SSL('${domain}') as imap:
+              imap.login('postmaster@${domain}', 'test')
+              imap.select()
+              status, refs = imap.search(None, 'ALL')
+              assert status == 'OK'
+              assert len(refs) == 1
+              status, msg = imap.fetch(refs[0], 'BODY[TEXT]')
+              assert status == 'OK'
+              assert msg[0][1].strip() == b"Hello World"
+        '')
+      ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("maddy.service")
+    server.wait_for_open_port(143)
+    server.wait_for_open_port(993)
+    server.wait_for_open_port(587)
+    server.wait_for_open_port(465)
+    client.succeed("send-testmail")
+    client.succeed("test-imap")
+  '';
+})
diff --git a/nixos/tests/maddy.nix b/nixos/tests/maddy/unencrypted.nix
index b9d0416482d..2420d461e4e 100644
--- a/nixos/tests/maddy.nix
+++ b/nixos/tests/maddy/unencrypted.nix
@@ -1,5 +1,5 @@
-import ./make-test-python.nix ({ pkgs, ... }: {
-  name = "maddy";
+import ../make-test-python.nix ({ pkgs, ... }: {
+  name = "maddy-unencrypted";
   meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; };
 
   nodes = {
@@ -9,6 +9,12 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         hostname = "server";
         primaryDomain = "server";
         openFirewall = true;
+        ensureAccounts = [ "postmaster@server" ];
+        ensureCredentials = {
+          # Do not use this in production. This will make passwords world-readable
+          # in the Nix store
+          "postmaster@server".passwordFile = "${pkgs.writeText "postmaster" "test"}";
+        };
       };
     };
 
@@ -48,10 +54,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
     server.wait_for_unit("maddy.service")
     server.wait_for_open_port(143)
     server.wait_for_open_port(587)
-
-    server.succeed("maddyctl creds create --password test postmaster@server")
-    server.succeed("maddyctl imap-acct create postmaster@server")
-
     client.succeed("send-testmail")
     client.succeed("test-imap")
   '';
diff --git a/nixos/tests/mate.nix b/nixos/tests/mate.nix
new file mode 100644
index 00000000000..78ba59c5fc2
--- /dev/null
+++ b/nixos/tests/mate.nix
@@ -0,0 +1,58 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "mate";
+
+  meta = {
+    maintainers = lib.teams.mate.members;
+  };
+
+  nodes.machine = { ... }: {
+    imports = [
+      ./common/user-account.nix
+    ];
+
+    services.xserver.enable = true;
+
+    services.xserver.displayManager = {
+      lightdm.enable = true;
+      autoLogin = {
+        enable = true;
+        user = "alice";
+      };
+    };
+
+    services.xserver.desktopManager.mate.enable = true;
+
+    # Silence log spam due to no sound drivers loaded:
+    # ALSA lib confmisc.c:855:(parse_card) cannot find card '0'
+    hardware.pulseaudio.enable = true;
+  };
+
+  testScript = { nodes, ... }:
+    let
+      user = nodes.machine.users.users.alice;
+    in
+    ''
+      with subtest("Wait for login"):
+          machine.wait_for_x()
+          machine.wait_for_file("${user.home}/.Xauthority")
+          machine.succeed("xauth merge ${user.home}/.Xauthority")
+
+      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("Check if MATE session components actually start"):
+          machine.wait_until_succeeds("pgrep marco")
+          machine.wait_for_window("marco")
+          machine.wait_until_succeeds("pgrep mate-panel")
+          machine.wait_for_window("Top Panel")
+          machine.wait_for_window("Bottom Panel")
+          machine.wait_until_succeeds("pgrep caja")
+          machine.wait_for_window("Caja")
+
+      with subtest("Open MATE terminal"):
+          machine.succeed("su - ${user.name} -c 'DISPLAY=:0.0 mate-terminal >&2 &'")
+          machine.wait_for_window("Terminal")
+          machine.sleep(20)
+          machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/matrix/mjolnir.nix b/nixos/tests/matrix/mjolnir.nix
index b1ac55d951c..c88113cb260 100644
--- a/nixos/tests/matrix/mjolnir.nix
+++ b/nixos/tests/matrix/mjolnir.nix
@@ -107,7 +107,10 @@ import ../make-test-python.nix (
       client = { pkgs, ... }: {
         environment.systemPackages = [
           (pkgs.writers.writePython3Bin "create_management_room_and_invite_mjolnir"
-            { libraries = [ pkgs.python3Packages.matrix-nio ]; } ''
+            { libraries = with pkgs.python3Packages; [
+                matrix-nio
+              ] ++ matrix-nio.optional-dependencies.e2e;
+            } ''
             import asyncio
 
             from nio import (
diff --git a/nixos/tests/mattermost.nix b/nixos/tests/mattermost.nix
index 49b418d9fff..e11201f0535 100644
--- a/nixos/tests/mattermost.nix
+++ b/nixos/tests/mattermost.nix
@@ -50,6 +50,13 @@ in
       mutableConfig = false;
       extraConfig.SupportSettings.HelpLink = "https://search.nixos.org";
     };
+    environmentFile = makeMattermost {
+      mutableConfig = false;
+      extraConfig.SupportSettings.AboutLink = "https://example.org";
+      environmentFile = pkgs.writeText "mattermost-env" ''
+        MM_SUPPORTSETTINGS_ABOUTLINK=https://nixos.org
+      '';
+    };
   };
 
   testScript = let
@@ -69,6 +76,7 @@ in
       rm -f $mattermostConfig
       echo "$newConfig" > "$mattermostConfig"
     '';
+
   in
   ''
     start_all()
@@ -120,5 +128,13 @@ in
 
     # Our edits should be ignored on restart
     immutable.succeed("${expectConfig ''.AboutLink == "https://nixos.org" and .HelpLink == "https://search.nixos.org"''}")
+
+
+    ## Environment File node tests ##
+    environmentFile.wait_for_unit("mattermost.service")
+    environmentFile.wait_for_open_port(8065)
+
+    # Settings in the environment file should override settings set otherwise
+    environmentFile.succeed("${expectConfig ''.AboutLink == "https://nixos.org"''}")
   '';
 })
diff --git a/nixos/tests/mediawiki.nix b/nixos/tests/mediawiki.nix
index 7f31d6aadfa..52122755ad9 100644
--- a/nixos/tests/mediawiki.nix
+++ b/nixos/tests/mediawiki.nix
@@ -1,28 +1,77 @@
-import ./make-test-python.nix ({ pkgs, lib, ... }: {
-  name = "mediawiki";
-  meta.maintainers = [ lib.maintainers.aanderse ];
-
-  nodes.machine =
-    { ... }:
-    { services.mediawiki.enable = true;
-      services.mediawiki.virtualHost.hostName = "localhost";
-      services.mediawiki.virtualHost.adminAddr = "root@example.com";
-      services.mediawiki.passwordFile = pkgs.writeText "password" "correcthorsebatterystaple";
-      services.mediawiki.extensions = {
-        Matomo = pkgs.fetchzip {
-          url = "https://github.com/DaSchTour/matomo-mediawiki-extension/archive/v4.0.1.tar.gz";
-          sha256 = "0g5rd3zp0avwlmqagc59cg9bbkn3r7wx7p6yr80s644mj6dlvs1b";
-        };
-        ParserFunctions = null;
+{
+  system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; },
+}:
+
+let
+  shared = {
+    services.mediawiki.enable = true;
+    services.mediawiki.httpd.virtualHost.hostName = "localhost";
+    services.mediawiki.httpd.virtualHost.adminAddr = "root@example.com";
+    services.mediawiki.passwordFile = pkgs.writeText "password" "correcthorsebatterystaple";
+    services.mediawiki.extensions = {
+      Matomo = pkgs.fetchzip {
+        url = "https://github.com/DaSchTour/matomo-mediawiki-extension/archive/v4.0.1.tar.gz";
+        sha256 = "0g5rd3zp0avwlmqagc59cg9bbkn3r7wx7p6yr80s644mj6dlvs1b";
       };
+      ParserFunctions = null;
+    };
+  };
+
+  testLib = import ../lib/testing-python.nix {
+    inherit system pkgs;
+    extraConfigurations = [ shared ];
+  };
+in
+{
+  mysql = testLib.makeTest {
+    name = "mediawiki-mysql";
+    nodes.machine = {
+      services.mediawiki.database.type = "mysql";
+    };
+    testScript = ''
+      start_all()
+
+      machine.wait_for_unit("phpfpm-mediawiki.service")
+
+      page = machine.succeed("curl -fL http://localhost/")
+      assert "MediaWiki has been installed" in page
+    '';
+  };
+
+  postgresql = testLib.makeTest {
+    name = "mediawiki-postgres";
+    nodes.machine = {
+      services.mediawiki.database.type = "postgres";
     };
+    testScript = ''
+      start_all()
 
-  testScript = ''
-    start_all()
+      machine.wait_for_unit("phpfpm-mediawiki.service")
 
-    machine.wait_for_unit("phpfpm-mediawiki.service")
+      page = machine.succeed("curl -fL http://localhost/")
+      assert "MediaWiki has been installed" in page
+    '';
+  };
 
-    page = machine.succeed("curl -fL http://localhost/")
-    assert "MediaWiki has been installed" in page
-  '';
-})
+  nohttpd = testLib.makeTest {
+    name = "mediawiki-nohttpd";
+    nodes.machine = {
+      services.mediawiki.webserver = "none";
+    };
+    testScript = { nodes, ... }: ''
+      start_all()
+      machine.wait_for_unit("phpfpm-mediawiki.service")
+      env = (
+        "SCRIPT_NAME=/index.php",
+        "SCRIPT_FILENAME=${nodes.machine.services.mediawiki.finalPackage}/share/mediawiki/index.php",
+        "REMOTE_ADDR=127.0.0.1",
+        'QUERY_STRING=title=Main_Page',
+        "REQUEST_METHOD=GET",
+      );
+      page = machine.succeed(f"{' '.join(env)} ${pkgs.fcgi}/bin/cgi-fcgi -bind -connect ${nodes.machine.services.phpfpm.pools.mediawiki.socket}")
+      assert "MediaWiki has been installed" in page, f"no 'MediaWiki has been installed' in:\n{page}"
+    '';
+  };
+}
diff --git a/nixos/tests/mindustry.nix b/nixos/tests/mindustry.nix
new file mode 100644
index 00000000000..b3f5423c601
--- /dev/null
+++ b/nixos/tests/mindustry.nix
@@ -0,0 +1,28 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "mindustry";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ fgaz ];
+  };
+
+  nodes.machine = { config, pkgs, ... }: {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    environment.systemPackages = [ pkgs.mindustry ];
+  };
+
+  enableOCR = true;
+
+  testScript =
+    ''
+      machine.wait_for_x()
+      machine.execute("mindustry >&2 &")
+      machine.wait_for_window("Mindustry")
+      # Loading can take a while. Avoid wasting cycles on OCR during that time
+      machine.sleep(60)
+      machine.wait_for_text(r"(Play|Database|Editor|Mods|Settings|Quit)")
+      machine.screenshot("screen")
+    '';
+})
diff --git a/nixos/tests/minio.nix b/nixos/tests/minio.nix
index ad51f738d49..ece4864f771 100644
--- a/nixos/tests/minio.nix
+++ b/nixos/tests/minio.nix
@@ -1,5 +1,5 @@
-import ./make-test-python.nix ({ pkgs, ...} :
-let
+import ./make-test-python.nix ({ pkgs, ... }:
+  let
     accessKey = "BKIKJAA5BMMU2RHO6IBB";
     secretKey = "V7f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12";
     minioPythonScript = pkgs.writeScript "minio-test.py" ''
@@ -18,41 +18,55 @@ let
       sio.seek(0)
       minioClient.put_object('test-bucket', 'test.txt', sio, sio_len, content_type='text/plain')
     '';
-in {
-  name = "minio";
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ bachp ];
-  };
+    rootCredentialsFile = "/etc/nixos/minio-root-credentials";
+    credsPartial = pkgs.writeText "minio-credentials-partial" ''
+      MINIO_ROOT_USER=${accessKey}
+    '';
+    credsFull = pkgs.writeText "minio-credentials-full" ''
+      MINIO_ROOT_USER=${accessKey}
+      MINIO_ROOT_PASSWORD=${secretKey}
+    '';
+  in
+  {
+    name = "minio";
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ bachp ];
+    };
 
-  nodes = {
-    machine = { pkgs, ... }: {
-      services.minio = {
-        enable = true;
-        rootCredentialsFile = pkgs.writeText "minio-credentials" ''
-          MINIO_ROOT_USER=${accessKey}
-          MINIO_ROOT_PASSWORD=${secretKey}
-        '';
-      };
-      environment.systemPackages = [ pkgs.minio-client ];
+    nodes = {
+      machine = { pkgs, ... }: {
+        services.minio = {
+          enable = true;
+          inherit rootCredentialsFile;
+        };
+        environment.systemPackages = [ pkgs.minio-client ];
 
-      # Minio requires at least 1GiB of free disk space to run.
-      virtualisation.diskSize = 4 * 1024;
+        # Minio requires at least 1GiB of free disk space to run.
+        virtualisation.diskSize = 4 * 1024;
+      };
     };
-  };
 
-  testScript = ''
-    start_all()
-    machine.wait_for_unit("minio.service")
-    machine.wait_for_open_port(9000)
+    testScript = ''
+      import time
+
+      start_all()
+      # simulate manually editing root credentials file
+      machine.wait_for_unit("multi-user.target")
+      machine.copy_from_host("${credsPartial}", "${rootCredentialsFile}")
+      time.sleep(3)
+      machine.copy_from_host("${credsFull}", "${rootCredentialsFile}")
 
-    # Create a test bucket on the server
-    machine.succeed(
-        "mc config host add minio http://localhost:9000 ${accessKey} ${secretKey} --api s3v4"
-    )
-    machine.succeed("mc mb minio/test-bucket")
-    machine.succeed("${minioPythonScript}")
-    assert "test-bucket" in machine.succeed("mc ls minio")
-    assert "Test from Python" in machine.succeed("mc cat minio/test-bucket/test.txt")
-    machine.shutdown()
-  '';
-})
+      machine.wait_for_unit("minio.service")
+      machine.wait_for_open_port(9000)
+
+      # Create a test bucket on the server
+      machine.succeed(
+          "mc config host add minio http://localhost:9000 ${accessKey} ${secretKey} --api s3v4"
+      )
+      machine.succeed("mc mb minio/test-bucket")
+      machine.succeed("${minioPythonScript}")
+      assert "test-bucket" in machine.succeed("mc ls minio")
+      assert "Test from Python" in machine.succeed("mc cat minio/test-bucket/test.txt")
+      machine.shutdown()
+    '';
+  })
diff --git a/nixos/tests/miriway.nix b/nixos/tests/miriway.nix
new file mode 100644
index 00000000000..d0d9f16d40f
--- /dev/null
+++ b/nixos/tests/miriway.nix
@@ -0,0 +1,130 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "miriway";
+
+  meta = {
+    maintainers = with lib.maintainers; [ OPNA2608 ];
+    # Natively running Mir has problems with capturing the first registered libinput device.
+    # In our VM  runners on ARM and on some hardware configs (my RPi4, distro-independent), this misses the keyboard.
+    # It can be worked around by dis- and reconnecting the affected hardware, but we can't do this in these tests.
+    # https://github.com/MirServer/mir/issues/2837
+    broken = pkgs.stdenv.hostPlatform.isAarch;
+  };
+
+  nodes.machine = { config, ... }: {
+    imports = [
+      ./common/auto.nix
+      ./common/user-account.nix
+    ];
+
+    # Seems to very rarely get interrupted by oom-killer
+    virtualisation.memorySize = 2047;
+
+    test-support.displayManager.auto = {
+      enable = true;
+      user = "alice";
+    };
+
+    services.xserver = {
+      enable = true;
+      displayManager.defaultSession = lib.mkForce "miriway";
+    };
+
+    programs.miriway = {
+      enable = true;
+      config = ''
+        add-wayland-extensions=all
+        enable-x11=
+
+        ctrl-alt=t:foot --maximized
+        ctrl-alt=a:env WINIT_UNIX_BACKEND=x11 WAYLAND_DISPLAY=invalid alacritty --option window.startup_mode=maximized
+
+        shell-component=dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY
+
+        shell-component=foot --maximized
+      '';
+    };
+
+    environment = {
+      shellAliases = {
+        test-wayland = "wayland-info | tee /tmp/test-wayland.out && touch /tmp/test-wayland-exit-ok";
+        test-x11 = "glinfo | tee /tmp/test-x11.out && touch /tmp/test-x11-exit-ok";
+      };
+
+      systemPackages = with pkgs; [
+        mesa-demos
+        wayland-utils
+        foot
+        alacritty
+      ];
+
+      # To help with OCR
+      etc."xdg/foot/foot.ini".text = lib.generators.toINI { } {
+        main = {
+          font = "inconsolata:size=16";
+        };
+        colors = rec {
+          foreground = "000000";
+          background = "ffffff";
+          regular2 = foreground;
+        };
+      };
+      etc."xdg/alacritty/alacritty.yml".text = lib.generators.toYAML { } {
+        font = rec {
+          normal.family = "Inconsolata";
+          bold.family = normal.family;
+          italic.family = normal.family;
+          bold_italic.family = normal.family;
+          size = 16;
+        };
+        colors = rec {
+          primary = {
+            foreground = "0x000000";
+            background = "0xffffff";
+          };
+          normal = {
+            green = primary.foreground;
+          };
+        };
+      };
+    };
+
+    fonts.fonts = [ pkgs.inconsolata ];
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }: ''
+    start_all()
+    machine.wait_for_unit("multi-user.target")
+
+    # Wait for Miriway to complete startup
+    machine.wait_for_file("/run/user/1000/wayland-0")
+    machine.succeed("pgrep miriway-shell")
+    machine.screenshot("miriway_launched")
+
+    # Test Wayland
+    # We let Miriway start the first terminal, as we might get stuck if it's not ready to process the first keybind
+    # machine.send_key("ctrl-alt-t")
+    machine.wait_for_text("alice@machine")
+    machine.send_chars("test-wayland\n")
+    machine.wait_for_file("/tmp/test-wayland-exit-ok")
+    machine.copy_from_vm("/tmp/test-wayland.out")
+    machine.screenshot("foot_wayland_info")
+    # Only succeeds when a mouse is moved inside an interactive session?
+    # machine.send_chars("exit\n")
+    # machine.wait_until_fails("pgrep foot")
+    machine.succeed("pkill foot")
+
+    # Test XWayland
+    machine.send_key("ctrl-alt-a")
+    machine.wait_for_text("alice@machine")
+    machine.send_chars("test-x11\n")
+    machine.wait_for_file("/tmp/test-x11-exit-ok")
+    machine.copy_from_vm("/tmp/test-x11.out")
+    machine.screenshot("alacritty_glinfo")
+    # Only succeeds when a mouse is moved inside an interactive session?
+    # machine.send_chars("exit\n")
+    # machine.wait_until_fails("pgrep alacritty")
+    machine.succeed("pkill alacritty")
+  '';
+})
diff --git a/nixos/tests/mongodb.nix b/nixos/tests/mongodb.nix
index edd074f5163..c26dc3ac32d 100644
--- a/nixos/tests/mongodb.nix
+++ b/nixos/tests/mongodb.nix
@@ -33,9 +33,6 @@ import ./make-test-python.nix ({ pkgs, ... }:
     nodes = {
       node = {...}: {
         environment.systemPackages = with pkgs; [
-          mongodb-3_4
-          mongodb-3_6
-          mongodb-4_0
           mongodb-4_2
           mongodb-4_4
           mongodb-5_0
@@ -46,9 +43,6 @@ import ./make-test-python.nix ({ pkgs, ... }:
     testScript = ''
       node.start()
     ''
-      + runMongoDBTest pkgs.mongodb-3_4
-      + 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
diff --git a/nixos/tests/multipass.nix b/nixos/tests/multipass.nix
new file mode 100644
index 00000000000..0980e9195f5
--- /dev/null
+++ b/nixos/tests/multipass.nix
@@ -0,0 +1,37 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  multipass-image = import ../release.nix {
+    configuration = {
+      # Building documentation makes the test unnecessarily take a longer time:
+      documentation.enable = lib.mkForce false;
+    };
+  };
+
+in
+{
+  name = "multipass";
+
+  meta.maintainers = [ lib.maintainers.jnsgruk ];
+
+  nodes.machine = { lib, ... }: {
+    virtualisation = {
+      cores = 1;
+      memorySize = 1024;
+      diskSize = 4096;
+
+      multipass.enable = true;
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("sockets.target")
+    machine.wait_for_unit("multipass.service")
+    machine.wait_for_file("/var/lib/multipass/data/multipassd/network/multipass_subnet")
+
+    # Wait for Multipass to settle
+    machine.sleep(1)
+
+    machine.succeed("multipass list")
+  '';
+})
diff --git a/nixos/tests/musescore.nix b/nixos/tests/musescore.nix
index ac2f4ba74c0..6aeb0558a49 100644
--- a/nixos/tests/musescore.nix
+++ b/nixos/tests/musescore.nix
@@ -2,13 +2,12 @@ import ./make-test-python.nix ({ pkgs, ...} :
 
 let
   # Make sure we don't have to go through the startup tutorial
-  customMuseScoreConfig = pkgs.writeText "MuseScore3.ini" ''
+  customMuseScoreConfig = pkgs.writeText "MuseScore4.ini" ''
     [application]
-    startup\firstStart=false
+    hasCompletedFirstLaunchSetup=true
 
-    [ui]
-    application\startup\showTours=false
-    application\startup\showStartCenter=false
+    [project]
+    preferredScoreCreationMode=1
     '';
 in
 {
@@ -40,26 +39,43 @@ in
     # Inject custom settings
     machine.succeed("mkdir -p /root/.config/MuseScore/")
     machine.succeed(
-        "cp ${customMuseScoreConfig} /root/.config/MuseScore/MuseScore3.ini"
+        "cp ${customMuseScoreConfig} /root/.config/MuseScore/MuseScore4.ini"
     )
 
     # Start MuseScore window
     machine.execute("DISPLAY=:0.0 mscore >&2 &")
 
     # Wait until MuseScore has launched
-    machine.wait_for_window("MuseScore")
+    machine.wait_for_window("MuseScore 4")
 
     # Wait until the window has completely initialised
-    machine.wait_for_text("MuseScore")
+    machine.wait_for_text("MuseScore 4")
+
+    machine.screenshot("MuseScore0")
+
+    # Create a new score
+    machine.send_key("ctrl-n")
+
+    # Wait until the creation wizard appears
+    machine.wait_for_window("New score")
+
+    machine.screenshot("MuseScore1")
+
+    machine.send_key("tab")
+    machine.send_key("tab")
+    machine.send_key("tab")
+    machine.send_key("tab")
+    machine.send_key("right")
+    machine.send_key("right")
+    machine.send_key("ret")
+
+    machine.sleep(1)
 
-    # Start entering notes
-    machine.send_key("n")
     # Type the beginning of https://de.wikipedia.org/wiki/Alle_meine_Entchen
     machine.send_chars("cdef6gg5aaaa7g")
-    # Make sure the VM catches up with all the keys
     machine.sleep(1)
 
-    machine.screenshot("MuseScore0")
+    machine.screenshot("MuseScore2")
 
     # Go to the export dialogue and create a PDF
     machine.send_key("alt-f")
@@ -67,24 +83,24 @@ in
     machine.send_key("e")
 
     # Wait until the export dialogue appears.
-    machine.wait_for_window("Export")
-    machine.screenshot("MuseScore1")
-    machine.send_key("shift-tab")
-    machine.sleep(1)
+    machine.wait_for_text("Export")
+
+    machine.screenshot("MuseScore3")
+
     machine.send_key("shift-tab")
     machine.sleep(1)
     machine.send_key("ret")
     machine.sleep(1)
     machine.send_key("ret")
 
-    machine.screenshot("MuseScore2")
+    machine.screenshot("MuseScore4")
 
     # Wait until PDF is exported
-    machine.wait_for_file("/root/Documents/MuseScore3/Scores/Untitled.pdf")
+    machine.wait_for_file('"/root/Documents/MuseScore4/Scores/Untitled score.pdf"')
 
     # Check that it contains the title of the score
-    machine.succeed("pdfgrep Title /root/Documents/MuseScore3/Scores/Untitled.pdf")
+    machine.succeed('pdfgrep "Untitled score" "/root/Documents/MuseScore4/Scores/Untitled score.pdf"')
 
-    machine.screenshot("MuseScore3")
+    machine.screenshot("MuseScore5")
   '';
 })
diff --git a/nixos/tests/n8n.nix b/nixos/tests/n8n.nix
index c1753a418f6..044240fbce7 100644
--- a/nixos/tests/n8n.nix
+++ b/nixos/tests/n8n.nix
@@ -19,7 +19,7 @@ in
 
   testScript = ''
     machine.wait_for_unit("n8n.service")
-    machine.wait_for_open_port(${toString port})
-    machine.succeed("curl --fail http://localhost:${toString port}/")
+    machine.wait_for_console_text("Editor is now accessible via")
+    machine.succeed("curl --fail -vvv http://localhost:${toString port}/")
   '';
 })
diff --git a/nixos/tests/nat.nix b/nixos/tests/nat.nix
index 545eb46f2bf..0b617cea777 100644
--- a/nixos/tests/nat.nix
+++ b/nixos/tests/nat.nix
@@ -3,26 +3,24 @@
 # client on the inside network, a server on the outside network, and a
 # router connected to both that performs Network Address Translation
 # for the client.
-import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ? false, ... }:
+import ./make-test-python.nix ({ pkgs, lib, withFirewall, nftables ? false, ... }:
   let
-    unit = if withFirewall then "firewall" else "nat";
+    unit = if nftables then "nftables" else (if withFirewall then "firewall" else "nat");
 
     routerBase =
       lib.mkMerge [
         { virtualisation.vlans = [ 2 1 ];
           networking.firewall.enable = withFirewall;
+          networking.firewall.filterForward = nftables;
+          networking.nftables.enable = nftables;
           networking.nat.internalIPs = [ "192.168.1.0/24" ];
           networking.nat.externalInterface = "eth1";
         }
-        (lib.optionalAttrs withConntrackHelpers {
-          networking.firewall.connectionTrackingModules = [ "ftp" ];
-          networking.firewall.autoLoadConntrackHelpers = true;
-        })
       ];
   in
   {
-    name = "nat" + (if withFirewall then "WithFirewall" else "Standalone")
-                 + (lib.optionalString withConntrackHelpers "withConntrackHelpers");
+    name = "nat" + (lib.optionalString nftables "Nftables")
+                 + (if withFirewall then "WithFirewall" else "Standalone");
     meta = with pkgs.lib.maintainers; {
       maintainers = [ eelco rob ];
     };
@@ -34,11 +32,8 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
             { virtualisation.vlans = [ 1 ];
               networking.defaultGateway =
                 (pkgs.lib.head nodes.router.config.networking.interfaces.eth2.ipv4.addresses).address;
+              networking.nftables.enable = nftables;
             }
-            (lib.optionalAttrs withConntrackHelpers {
-              networking.firewall.connectionTrackingModules = [ "ftp" ];
-              networking.firewall.autoLoadConntrackHelpers = true;
-            })
           ];
 
         router =
@@ -91,7 +86,7 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
         client.succeed("curl -v ftp://server/foo.txt >&2")
 
         # Test whether active FTP works.
-        client.${if withConntrackHelpers then "succeed" else "fail"}("curl -v -P - ftp://server/foo.txt >&2")
+        client.fail("curl -v -P - ftp://server/foo.txt >&2")
 
         # Test ICMP.
         client.succeed("ping -c 1 router >&2")
@@ -111,7 +106,7 @@ import ./make-test-python.nix ({ pkgs, lib, withFirewall, withConntrackHelpers ?
         # FIXME: this should not be necessary, but nat.service is not started because
         #        network.target is not triggered
         #        (https://github.com/NixOS/nixpkgs/issues/16230#issuecomment-226408359)
-        ${lib.optionalString (!withFirewall) ''
+        ${lib.optionalString (!withFirewall && !nftables) ''
           router.succeed("systemctl start nat.service")
         ''}
         client.succeed("curl --fail http://server/ >&2")
diff --git a/nixos/tests/nebula.nix b/nixos/tests/nebula.nix
index 372cfebdf80..89b91d89fcb 100644
--- a/nixos/tests/nebula.nix
+++ b/nixos/tests/nebula.nix
@@ -10,6 +10,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: let
       environment.systemPackages = [ pkgs.nebula ];
       users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
       services.openssh.enable = true;
+      networking.interfaces.eth1.useDHCP = false;
 
       services.nebula.networks.smoke = {
         # Note that these paths won't exist when the machine is first booted.
@@ -30,13 +31,14 @@ in
 
     lighthouse = { ... } @ args:
       makeNebulaNode args "lighthouse" {
-        networking.interfaces.eth1.ipv4.addresses = [{
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [{
           address = "192.168.1.1";
           prefixLength = 24;
         }];
 
         services.nebula.networks.smoke = {
           isLighthouse = true;
+          isRelay = true;
           firewall = {
             outbound = [ { port = "any"; proto = "any"; host = "any"; } ];
             inbound = [ { port = "any"; proto = "any"; host = "any"; } ];
@@ -44,9 +46,9 @@ in
         };
       };
 
-    node2 = { ... } @ args:
-      makeNebulaNode args "node2" {
-        networking.interfaces.eth1.ipv4.addresses = [{
+    allowAny = { ... } @ args:
+      makeNebulaNode args "allowAny" {
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [{
           address = "192.168.1.2";
           prefixLength = 24;
         }];
@@ -55,6 +57,7 @@ in
           staticHostMap = { "10.0.100.1" = [ "192.168.1.1:4242" ]; };
           isLighthouse = false;
           lighthouses = [ "10.0.100.1" ];
+          relays = [ "10.0.100.1" ];
           firewall = {
             outbound = [ { port = "any"; proto = "any"; host = "any"; } ];
             inbound = [ { port = "any"; proto = "any"; host = "any"; } ];
@@ -62,9 +65,9 @@ in
         };
       };
 
-    node3 = { ... } @ args:
-      makeNebulaNode args "node3" {
-        networking.interfaces.eth1.ipv4.addresses = [{
+    allowFromLighthouse = { ... } @ args:
+      makeNebulaNode args "allowFromLighthouse" {
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [{
           address = "192.168.1.3";
           prefixLength = 24;
         }];
@@ -73,6 +76,7 @@ in
           staticHostMap = { "10.0.100.1" = [ "192.168.1.1:4242" ]; };
           isLighthouse = false;
           lighthouses = [ "10.0.100.1" ];
+          relays = [ "10.0.100.1" ];
           firewall = {
             outbound = [ { port = "any"; proto = "any"; host = "any"; } ];
             inbound = [ { port = "any"; proto = "any"; host = "lighthouse"; } ];
@@ -80,9 +84,9 @@ in
         };
       };
 
-    node4 = { ... } @ args:
-      makeNebulaNode args "node4" {
-        networking.interfaces.eth1.ipv4.addresses = [{
+    allowToLighthouse = { ... } @ args:
+      makeNebulaNode args "allowToLighthouse" {
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [{
           address = "192.168.1.4";
           prefixLength = 24;
         }];
@@ -92,6 +96,7 @@ in
           staticHostMap = { "10.0.100.1" = [ "192.168.1.1:4242" ]; };
           isLighthouse = false;
           lighthouses = [ "10.0.100.1" ];
+          relays = [ "10.0.100.1" ];
           firewall = {
             outbound = [ { port = "any"; proto = "any"; host = "lighthouse"; } ];
             inbound = [ { port = "any"; proto = "any"; host = "any"; } ];
@@ -99,9 +104,9 @@ in
         };
       };
 
-    node5 = { ... } @ args:
-      makeNebulaNode args "node5" {
-        networking.interfaces.eth1.ipv4.addresses = [{
+    disabled = { ... } @ args:
+      makeNebulaNode args "disabled" {
+        networking.interfaces.eth1.ipv4.addresses = lib.mkForce [{
           address = "192.168.1.5";
           prefixLength = 24;
         }];
@@ -111,6 +116,7 @@ in
           staticHostMap = { "10.0.100.1" = [ "192.168.1.1:4242" ]; };
           isLighthouse = false;
           lighthouses = [ "10.0.100.1" ];
+          relays = [ "10.0.100.1" ];
           firewall = {
             outbound = [ { port = "any"; proto = "any"; host = "lighthouse"; } ];
             inbound = [ { port = "any"; proto = "any"; host = "any"; } ];
@@ -123,12 +129,14 @@ in
   testScript = let
 
     setUpPrivateKey = name: ''
-    ${name}.succeed(
-        "mkdir -p /root/.ssh",
-        "chown 700 /root/.ssh",
-        "cat '${snakeOilPrivateKey}' > /root/.ssh/id_snakeoil",
-        "chown 600 /root/.ssh/id_snakeoil",
-    )
+      ${name}.start()
+      ${name}.succeed(
+          "mkdir -p /root/.ssh",
+          "chown 700 /root/.ssh",
+          "cat '${snakeOilPrivateKey}' > /root/.ssh/id_snakeoil",
+          "chown 600 /root/.ssh/id_snakeoil",
+          "mkdir -p /root"
+      )
     '';
 
     # From what I can tell, StrictHostKeyChecking=no is necessary for ssh to work between machines.
@@ -146,26 +154,48 @@ in
       ${name}.succeed(
           "mkdir -p /etc/nebula",
           "nebula-cert keygen -out-key /etc/nebula/${name}.key -out-pub /etc/nebula/${name}.pub",
-          "scp ${sshOpts} /etc/nebula/${name}.pub 192.168.1.1:/tmp/${name}.pub",
+          "scp ${sshOpts} /etc/nebula/${name}.pub root@192.168.1.1:/root/${name}.pub",
       )
       lighthouse.succeed(
-          'nebula-cert sign -ca-crt /etc/nebula/ca.crt -ca-key /etc/nebula/ca.key -name "${name}" -groups "${name}" -ip "${ip}" -in-pub /tmp/${name}.pub -out-crt /tmp/${name}.crt',
+          'nebula-cert sign -ca-crt /etc/nebula/ca.crt -ca-key /etc/nebula/ca.key -name "${name}" -groups "${name}" -ip "${ip}" -in-pub /root/${name}.pub -out-crt /root/${name}.crt'
       )
       ${name}.succeed(
-          "scp ${sshOpts} 192.168.1.1:/tmp/${name}.crt /etc/nebula/${name}.crt",
-          "scp ${sshOpts} 192.168.1.1:/etc/nebula/ca.crt /etc/nebula/ca.crt",
+          "scp ${sshOpts} root@192.168.1.1:/root/${name}.crt /etc/nebula/${name}.crt",
+          "scp ${sshOpts} root@192.168.1.1:/etc/nebula/ca.crt /etc/nebula/ca.crt",
+          '(id nebula-smoke >/dev/null && chown -R nebula-smoke:nebula-smoke /etc/nebula) || true'
       )
     '';
 
-  in ''
-    start_all()
+    getPublicIp = node: ''
+      ${node}.succeed("ip --brief addr show eth1 | awk '{print $3}' | tail -n1 | cut -d/ -f1").strip()
+    '';
 
+    # Never do this for anything security critical! (Thankfully it's just a test.)
+    # Restart Nebula right after the mutual block and/or restore so the state is fresh.
+    blockTrafficBetween = nodeA: nodeB: ''
+      node_a = ${getPublicIp nodeA}
+      node_b = ${getPublicIp nodeB}
+      ${nodeA}.succeed("iptables -I INPUT -s " + node_b + " -j DROP")
+      ${nodeB}.succeed("iptables -I INPUT -s " + node_a + " -j DROP")
+      ${nodeA}.systemctl("restart nebula@smoke.service")
+      ${nodeB}.systemctl("restart nebula@smoke.service")
+    '';
+    allowTrafficBetween = nodeA: nodeB: ''
+      node_a = ${getPublicIp nodeA}
+      node_b = ${getPublicIp nodeB}
+      ${nodeA}.succeed("iptables -D INPUT -s " + node_b + " -j DROP")
+      ${nodeB}.succeed("iptables -D INPUT -s " + node_a + " -j DROP")
+      ${nodeA}.systemctl("restart nebula@smoke.service")
+      ${nodeB}.systemctl("restart nebula@smoke.service")
+    '';
+  in ''
     # Create the certificate and sign the lighthouse's keys.
     ${setUpPrivateKey "lighthouse"}
     lighthouse.succeed(
         "mkdir -p /etc/nebula",
         'nebula-cert ca -name "Smoke Test" -out-crt /etc/nebula/ca.crt -out-key /etc/nebula/ca.key',
         'nebula-cert sign -ca-crt /etc/nebula/ca.crt -ca-key /etc/nebula/ca.key -name "lighthouse" -groups "lighthouse" -ip "10.0.100.1/24" -out-crt /etc/nebula/lighthouse.crt -out-key /etc/nebula/lighthouse.key',
+        'chown -R nebula-smoke:nebula-smoke /etc/nebula'
     )
 
     # Reboot the lighthouse and verify that the nebula service comes up on boot.
@@ -175,49 +205,104 @@ in
     lighthouse.wait_for_unit("nebula@smoke.service")
     lighthouse.succeed("ping -c5 10.0.100.1")
 
-    # Create keys for node2's nebula service and test that it comes up.
-    ${setUpPrivateKey "node2"}
-    ${signKeysFor "node2" "10.0.100.2/24"}
-    ${restartAndCheckNebula "node2" "10.0.100.2"}
+    # Create keys for allowAny's nebula service and test that it comes up.
+    ${setUpPrivateKey "allowAny"}
+    ${signKeysFor "allowAny" "10.0.100.2/24"}
+    ${restartAndCheckNebula "allowAny" "10.0.100.2"}
 
-    # Create keys for node3's nebula service and test that it comes up.
-    ${setUpPrivateKey "node3"}
-    ${signKeysFor "node3" "10.0.100.3/24"}
-    ${restartAndCheckNebula "node3" "10.0.100.3"}
+    # Create keys for allowFromLighthouse's nebula service and test that it comes up.
+    ${setUpPrivateKey "allowFromLighthouse"}
+    ${signKeysFor "allowFromLighthouse" "10.0.100.3/24"}
+    ${restartAndCheckNebula "allowFromLighthouse" "10.0.100.3"}
 
-    # Create keys for node4's nebula service and test that it comes up.
-    ${setUpPrivateKey "node4"}
-    ${signKeysFor "node4" "10.0.100.4/24"}
-    ${restartAndCheckNebula "node4" "10.0.100.4"}
+    # Create keys for allowToLighthouse's nebula service and test that it comes up.
+    ${setUpPrivateKey "allowToLighthouse"}
+    ${signKeysFor "allowToLighthouse" "10.0.100.4/24"}
+    ${restartAndCheckNebula "allowToLighthouse" "10.0.100.4"}
 
-    # Create keys for node4's nebula service and test that it does not come up.
-    ${setUpPrivateKey "node5"}
-    ${signKeysFor "node5" "10.0.100.5/24"}
-    node5.fail("systemctl status nebula@smoke.service")
-    node5.fail("ping -c5 10.0.100.5")
+    # Create keys for disabled's nebula service and test that it does not come up.
+    ${setUpPrivateKey "disabled"}
+    ${signKeysFor "disabled" "10.0.100.5/24"}
+    disabled.fail("systemctl status nebula@smoke.service")
+    disabled.fail("ping -c5 10.0.100.5")
 
-    # The lighthouse can ping node2 and node3 but not node5
+    # The lighthouse can ping allowAny and allowFromLighthouse but not disabled
     lighthouse.succeed("ping -c3 10.0.100.2")
     lighthouse.succeed("ping -c3 10.0.100.3")
     lighthouse.fail("ping -c3 10.0.100.5")
 
-    # node2 can ping the lighthouse, but not node3 because of its inbound firewall
-    node2.succeed("ping -c3 10.0.100.1")
-    node2.fail("ping -c3 10.0.100.3")
-
-    # node3 can ping the lighthouse and node2
-    node3.succeed("ping -c3 10.0.100.1")
-    node3.succeed("ping -c3 10.0.100.2")
-
-    # node4 can ping the lighthouse but not node2 or node3
-    node4.succeed("ping -c3 10.0.100.1")
-    node4.fail("ping -c3 10.0.100.2")
-    node4.fail("ping -c3 10.0.100.3")
-
-    # node2 can ping node3 now that node3 pinged it first
-    node2.succeed("ping -c3 10.0.100.3")
-    # node4 can ping node2 if node2 pings it first
-    node2.succeed("ping -c3 10.0.100.4")
-    node4.succeed("ping -c3 10.0.100.2")
+    # allowAny can ping the lighthouse, but not allowFromLighthouse because of its inbound firewall
+    allowAny.succeed("ping -c3 10.0.100.1")
+    allowAny.fail("ping -c3 10.0.100.3")
+
+    # allowFromLighthouse can ping the lighthouse and allowAny
+    allowFromLighthouse.succeed("ping -c3 10.0.100.1")
+    allowFromLighthouse.succeed("ping -c3 10.0.100.2")
+
+    # block allowFromLighthouse <-> allowAny, and allowFromLighthouse -> allowAny should still work.
+    ${blockTrafficBetween "allowFromLighthouse" "allowAny"}
+    allowFromLighthouse.succeed("ping -c10 10.0.100.2")
+    ${allowTrafficBetween "allowFromLighthouse" "allowAny"}
+    allowFromLighthouse.succeed("ping -c10 10.0.100.2")
+
+    # allowToLighthouse can ping the lighthouse but not allowAny or allowFromLighthouse
+    allowToLighthouse.succeed("ping -c3 10.0.100.1")
+    allowToLighthouse.fail("ping -c3 10.0.100.2")
+    allowToLighthouse.fail("ping -c3 10.0.100.3")
+
+    # allowAny can ping allowFromLighthouse now that allowFromLighthouse pinged it first
+    allowAny.succeed("ping -c3 10.0.100.3")
+
+    # block allowAny <-> allowFromLighthouse, and allowAny -> allowFromLighthouse should still work.
+    ${blockTrafficBetween "allowAny" "allowFromLighthouse"}
+    allowFromLighthouse.succeed("ping -c10 10.0.100.2")
+    allowAny.succeed("ping -c10 10.0.100.3")
+    ${allowTrafficBetween "allowAny" "allowFromLighthouse"}
+    allowFromLighthouse.succeed("ping -c10 10.0.100.2")
+    allowAny.succeed("ping -c10 10.0.100.3")
+
+    # allowToLighthouse can ping allowAny if allowAny pings it first
+    allowAny.succeed("ping -c3 10.0.100.4")
+    allowToLighthouse.succeed("ping -c3 10.0.100.2")
+
+    # block allowToLighthouse <-> allowAny, and allowAny <-> allowToLighthouse should still work.
+    ${blockTrafficBetween "allowAny" "allowToLighthouse"}
+    allowAny.succeed("ping -c10 10.0.100.4")
+    allowToLighthouse.succeed("ping -c10 10.0.100.2")
+    ${allowTrafficBetween "allowAny" "allowToLighthouse"}
+    allowAny.succeed("ping -c10 10.0.100.4")
+    allowToLighthouse.succeed("ping -c10 10.0.100.2")
+
+    # block lighthouse <-> allowFromLighthouse and allowAny <-> allowFromLighthouse; allowFromLighthouse won't get to allowAny
+    ${blockTrafficBetween "allowFromLighthouse" "lighthouse"}
+    ${blockTrafficBetween "allowFromLighthouse" "allowAny"}
+    allowFromLighthouse.fail("ping -c3 10.0.100.2")
+    ${allowTrafficBetween "allowFromLighthouse" "lighthouse"}
+    ${allowTrafficBetween "allowFromLighthouse" "allowAny"}
+    allowFromLighthouse.succeed("ping -c3 10.0.100.2")
+
+    # block lighthouse <-> allowAny, allowAny <-> allowFromLighthouse, and allowAny <-> allowToLighthouse; it won't get to allowFromLighthouse or allowToLighthouse
+    ${blockTrafficBetween "allowAny" "lighthouse"}
+    ${blockTrafficBetween "allowAny" "allowFromLighthouse"}
+    ${blockTrafficBetween "allowAny" "allowToLighthouse"}
+    allowFromLighthouse.fail("ping -c3 10.0.100.2")
+    allowAny.fail("ping -c3 10.0.100.3")
+    allowAny.fail("ping -c3 10.0.100.4")
+    ${allowTrafficBetween "allowAny" "lighthouse"}
+    ${allowTrafficBetween "allowAny" "allowFromLighthouse"}
+    ${allowTrafficBetween "allowAny" "allowToLighthouse"}
+    allowFromLighthouse.succeed("ping -c3 10.0.100.2")
+    allowAny.succeed("ping -c3 10.0.100.3")
+    allowAny.succeed("ping -c3 10.0.100.4")
+
+    # block lighthouse <-> allowToLighthouse and allowToLighthouse <-> allowAny; it won't get to allowAny
+    ${blockTrafficBetween "allowToLighthouse" "lighthouse"}
+    ${blockTrafficBetween "allowToLighthouse" "allowAny"}
+    allowAny.fail("ping -c3 10.0.100.4")
+    allowToLighthouse.fail("ping -c3 10.0.100.2")
+    ${allowTrafficBetween "allowToLighthouse" "lighthouse"}
+    ${allowTrafficBetween "allowToLighthouse" "allowAny"}
+    allowAny.succeed("ping -c3 10.0.100.4")
+    allowToLighthouse.succeed("ping -c3 10.0.100.2")
   '';
 })
diff --git a/nixos/tests/netdata.nix b/nixos/tests/netdata.nix
index 0f26630da9d..aea67c29d0d 100644
--- a/nixos/tests/netdata.nix
+++ b/nixos/tests/netdata.nix
@@ -3,7 +3,7 @@
 import ./make-test-python.nix ({ pkgs, ...} : {
   name = "netdata";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ cransom ];
+    maintainers = [ cransom raitobezarius ];
   };
 
   nodes = {
diff --git a/nixos/tests/networking.nix b/nixos/tests/networking.nix
index 71b82b87127..441d258afc0 100644
--- a/nixos/tests/networking.nix
+++ b/nixos/tests/networking.nix
@@ -971,6 +971,7 @@ let
         print(machine.succeed("ip link show dev custom_name"))
       '';
     };
+      nodes = { };
     # even with disabled networkd, systemd.network.links should work
     # (as it's handled by udev, not networkd)
     link = {
diff --git a/nixos/tests/nextcloud/default.nix b/nixos/tests/nextcloud/default.nix
index b8d3ba75b51..78fe026b4a8 100644
--- a/nixos/tests/nextcloud/default.nix
+++ b/nixos/tests/nextcloud/default.nix
@@ -26,4 +26,4 @@ foldl
     };
   })
 { }
-  [ 24 25 ]
+  [ 25 26 ]
diff --git a/nixos/tests/nextcloud/openssl-sse.nix b/nixos/tests/nextcloud/openssl-sse.nix
index 7595ee2c67e..871947e1d2b 100644
--- a/nixos/tests/nextcloud/openssl-sse.nix
+++ b/nixos/tests/nextcloud/openssl-sse.nix
@@ -55,6 +55,7 @@ in {
     nextcloudwithopenssl1.wait_for_unit("multi-user.target")
     nextcloudwithopenssl1.succeed("nextcloud-occ status")
     nextcloudwithopenssl1.succeed("curl -sSf http://nextcloudwithopenssl1/login")
+    nextcloud_version = ${toString nextcloudVersion}
 
     with subtest("With OpenSSL 1 SSE can be enabled and used"):
         nextcloudwithopenssl1.succeed("nextcloud-occ app:enable encryption")
@@ -71,7 +72,9 @@ in {
         nextcloudwithopenssl1.succeed("nextcloud-occ status")
 
     with subtest("Existing encrypted files cannot be read, but new files can be added"):
-        nextcloudwithopenssl1.fail("${withRcloneEnv3} ${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file >&2")
+        # This will succed starting NC26 because of their custom implementation of openssl_seal
+        read_existing_file_test = nextcloudwithopenssl1.fail if nextcloud_version < 26 else nextcloudwithopenssl1.succeed
+        read_existing_file_test("${withRcloneEnv3} ${pkgs.rclone}/bin/rclone cat nextcloud:test-shared-file >&2")
         nextcloudwithopenssl1.succeed("nextcloud-occ encryption:disable")
         nextcloudwithopenssl1.succeed("${copySharedFile3}")
         nextcloudwithopenssl1.succeed("grep bye /var/lib/nextcloud/data/root/files/test-shared-file2")
diff --git a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix
index 93e655c3056..ce0019e9da4 100644
--- a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix
+++ b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix
@@ -1,6 +1,11 @@
 import ../make-test-python.nix ({ pkgs, ...}: let
-  adminpass = "hunter2";
-  adminuser = "custom-admin-username";
+  username = "custom_admin_username";
+  # This will be used both for redis and postgresql
+  pass = "hunter2";
+  # Don't do this at home, use a file outside of the nix store instead
+  passFile = toString (pkgs.writeText "pass-file" ''
+    ${pass}
+  '');
 in {
   name = "nextcloud-with-declarative-redis";
   meta = with pkgs.lib.maintainers; {
@@ -22,15 +27,15 @@ in {
           redis = true;
           memcached = false;
         };
+        # This test also validates that we can use an "external" database
+        database.createLocally = false;
         config = {
           dbtype = "pgsql";
           dbname = "nextcloud";
-          dbuser = "nextcloud";
-          dbhost = "/run/postgresql";
-          inherit adminuser;
-          adminpassFile = toString (pkgs.writeText "admin-pass-file" ''
-            ${adminpass}
-          '');
+          dbuser = username;
+          dbpassFile = passFile;
+          adminuser = username;
+          adminpassFile = passFile;
         };
         secretFile = "/etc/nextcloud-secrets.json";
 
@@ -52,20 +57,20 @@ in {
 
       systemd.services.nextcloud-setup= {
         requires = ["postgresql.service"];
-        after = [
-          "postgresql.service"
-        ];
+        after = [ "postgresql.service" ];
       };
 
       services.postgresql = {
         enable = true;
-        ensureDatabases = [ "nextcloud" ];
-        ensureUsers = [
-          { name = "nextcloud";
-            ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
-          }
-        ];
       };
+      systemd.services.postgresql.postStart = pkgs.lib.mkAfter ''
+        password=$(cat ${passFile})
+        ${config.services.postgresql.package}/bin/psql <<EOF
+          CREATE ROLE ${username} WITH LOGIN PASSWORD '$password' CREATEDB;
+          CREATE DATABASE nextcloud;
+          GRANT ALL PRIVILEGES ON DATABASE nextcloud TO ${username};
+        EOF
+      '';
 
       # This file is meant to contain secret options which should
       # not go into the nix store. Here it is just used to set the
@@ -86,8 +91,8 @@ in {
       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})"
+      export RCLONE_CONFIG_NEXTCLOUD_USER="${username}"
+      export RCLONE_CONFIG_NEXTCLOUD_PASS="$(${pkgs.rclone}/bin/rclone obscure ${pass})"
       "''${@}"
     '';
     copySharedFile = pkgs.writeScript "copy-shared-file" ''
diff --git a/nixos/tests/nextcloud/with-mysql-and-memcached.nix b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
index 63e0e2c5963..f673e5e75d3 100644
--- a/nixos/tests/nextcloud/with-mysql-and-memcached.nix
+++ b/nixos/tests/nextcloud/with-mysql-and-memcached.nix
@@ -26,24 +26,13 @@ in {
           redis = false;
           memcached = true;
         };
-        database.createLocally = true;
         config = {
           dbtype = "mysql";
-          dbname = "nextcloud";
-          dbuser = "nextcloud";
-          dbhost = "127.0.0.1";
-          dbport = 3306;
-          dbpassFile = "${pkgs.writeText "dbpass" "hunter2" }";
           # Don't inherit adminuser since "root" is supposed to be the default
           adminpassFile = "${pkgs.writeText "adminpass" adminpass}"; # Don't try this at home!
         };
       };
 
-      systemd.services.nextcloud-setup= {
-        requires = ["mysql.service"];
-        after = ["mysql.service"];
-      };
-
       services.memcached.enable = true;
     };
   };
diff --git a/nixos/tests/nextcloud/with-postgresql-and-redis.nix b/nixos/tests/nextcloud/with-postgresql-and-redis.nix
index 1ef848cfb12..43892d39e9f 100644
--- a/nixos/tests/nextcloud/with-postgresql-and-redis.nix
+++ b/nixos/tests/nextcloud/with-postgresql-and-redis.nix
@@ -13,7 +13,7 @@ in {
     # The only thing the client needs to do is download a file.
     client = { ... }: {};
 
-    nextcloud = { config, pkgs, ... }: {
+    nextcloud = { config, pkgs, lib, ... }: {
       networking.firewall.allowedTCPPorts = [ 80 ];
 
       services.nextcloud = {
@@ -27,35 +27,24 @@ in {
         };
         config = {
           dbtype = "pgsql";
-          dbname = "nextcloud";
-          dbuser = "nextcloud";
-          dbhost = "/run/postgresql";
           inherit adminuser;
           adminpassFile = toString (pkgs.writeText "admin-pass-file" ''
             ${adminpass}
           '');
+          trustedProxies = [ "::1" ];
+        };
+        notify_push = {
+          enable = true;
+          logLevel = "debug";
+        };
+        extraAppsEnable = true;
+        extraApps = {
+          inherit (pkgs."nextcloud${lib.versions.major config.services.nextcloud.package.version}Packages".apps) notify_push;
         };
       };
 
       services.redis.servers."nextcloud".enable = true;
       services.redis.servers."nextcloud".port = 6379;
-
-      systemd.services.nextcloud-setup= {
-        requires = ["postgresql.service"];
-        after = [
-          "postgresql.service"
-        ];
-      };
-
-      services.postgresql = {
-        enable = true;
-        ensureDatabases = [ "nextcloud" ];
-        ensureUsers = [
-          { name = "nextcloud";
-            ensurePermissions."DATABASE nextcloud" = "ALL PRIVILEGES";
-          }
-        ];
-      };
     };
   };
 
@@ -94,8 +83,10 @@ in {
         "${withRcloneEnv} ${copySharedFile}"
     )
     client.wait_for_unit("multi-user.target")
+    client.execute("${pkgs.nextcloud-notify_push.passthru.test_client}/bin/test_client http://nextcloud ${adminuser} ${adminpass} >&2 &")
     client.succeed(
         "${withRcloneEnv} ${diffSharedFile}"
     )
+    nextcloud.wait_until_succeeds("journalctl -u nextcloud-notify_push | grep -q \"Sending ping to ${adminuser}\"")
   '';
 })) args
diff --git a/nixos/tests/nginx-http3.nix b/nixos/tests/nginx-http3.nix
index 319f6aac184..f003130b46f 100644
--- a/nixos/tests/nginx-http3.nix
+++ b/nixos/tests/nginx-http3.nix
@@ -36,8 +36,10 @@ in
           sslCertificateKey = ./common/acme/server/acme.test.key.pem;
           http2 = true;
           http3 = true;
+          http3_hq = false;
+          quic = true;
           reuseport = true;
-          root = lib.mkForce (pkgs.runCommandLocal "testdir2" {} ''
+          root = lib.mkForce (pkgs.runCommandLocal "testdir" {} ''
             mkdir "$out"
             cat > "$out/index.html" <<EOF
             <html><body>Hello World!</body></html>
@@ -82,6 +84,8 @@ in
 
     # Check header reading
     client.succeed("curl --verbose --http3 --head https://acme.test | grep 'content-type'")
+    client.succeed("curl --verbose --http3 --head https://acme.test | grep 'HTTP/3 200'")
+    client.succeed("curl --verbose --http3 --head https://acme.test/error | grep 'HTTP/3 404'")
 
     # Check change User-Agent
     client.succeed("curl --verbose --http3 --user-agent 'Curl test 3.0' https://acme.test")
diff --git a/nixos/tests/nginx.nix b/nixos/tests/nginx.nix
index 73f1133bd6c..2a7e0f48d86 100644
--- a/nixos/tests/nginx.nix
+++ b/nixos/tests/nginx.nix
@@ -61,16 +61,16 @@ import ./make-test-python.nix ({ pkgs, ... }: {
 
       specialisation.reloadWithErrorsSystem.configuration = {
         services.nginx.package = pkgs.nginxMainline;
-        services.nginx.virtualHosts."hello".extraConfig = "access_log /does/not/exist.log;";
+        services.nginx.virtualHosts."!@$$(#*%".locations."~@#*$*!)".proxyPass = ";;;";
       };
     };
   };
 
   testScript = { nodes, ... }: let
-    etagSystem = "${nodes.webserver.config.system.build.toplevel}/specialisation/etagSystem";
-    justReloadSystem = "${nodes.webserver.config.system.build.toplevel}/specialisation/justReloadSystem";
-    reloadRestartSystem = "${nodes.webserver.config.system.build.toplevel}/specialisation/reloadRestartSystem";
-    reloadWithErrorsSystem = "${nodes.webserver.config.system.build.toplevel}/specialisation/reloadWithErrorsSystem";
+    etagSystem = "${nodes.webserver.system.build.toplevel}/specialisation/etagSystem";
+    justReloadSystem = "${nodes.webserver.system.build.toplevel}/specialisation/justReloadSystem";
+    reloadRestartSystem = "${nodes.webserver.system.build.toplevel}/specialisation/reloadRestartSystem";
+    reloadWithErrorsSystem = "${nodes.webserver.system.build.toplevel}/specialisation/reloadWithErrorsSystem";
   in ''
     url = "http://localhost/index.html"
 
diff --git a/nixos/tests/nixops/default.nix b/nixos/tests/nixops/default.nix
index b77ac247639..bd00e614363 100644
--- a/nixos/tests/nixops/default.nix
+++ b/nixos/tests/nixops/default.nix
@@ -30,12 +30,10 @@ let
         virtualisation.additionalPaths = [
           pkgs.hello
           pkgs.figlet
-
-          # This includes build dependencies all the way down. Not efficient,
-          # but we do need build deps to an *arbitrary* depth, which is hard to
-          # determine.
-          (allDrvOutputs nodes.server.config.system.build.toplevel)
         ];
+
+        # TODO: make this efficient, https://github.com/NixOS/nixpkgs/issues/180529
+        system.includeBuildDependencies = true;
       };
       server = { lib, ... }: {
         imports = [ ./legacy/base-configuration.nix ];
diff --git a/nixos/tests/nixos-rebuild-specialisations.nix b/nixos/tests/nixos-rebuild-specialisations.nix
new file mode 100644
index 00000000000..c61f81b8ca6
--- /dev/null
+++ b/nixos/tests/nixos-rebuild-specialisations.nix
@@ -0,0 +1,131 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "nixos-rebuild-specialisations";
+
+  nodes = {
+    machine = { lib, pkgs, ... }: {
+      imports = [
+        ../modules/profiles/installation-device.nix
+        ../modules/profiles/base.nix
+      ];
+
+      nix.settings = {
+        substituters = lib.mkForce [ ];
+        hashed-mirrors = null;
+        connect-timeout = 1;
+      };
+
+      system.extraDependencies = with pkgs; [
+        curl
+        desktop-file-utils
+        docbook5
+        docbook_xsl_ns
+        grub2
+        kmod.dev
+        libarchive
+        libarchive.dev
+        libxml2.bin
+        libxslt.bin
+        python3Minimal
+        shared-mime-info
+        stdenv
+        sudo
+        xorg.lndir
+      ];
+
+      virtualisation = {
+        cores = 2;
+        memorySize = 2048;
+      };
+    };
+  };
+
+  testScript =
+    let
+      configFile = pkgs.writeText "configuration.nix" ''
+        { lib, pkgs, ... }: {
+          imports = [
+            ./hardware-configuration.nix
+            <nixpkgs/nixos/modules/testing/test-instrumentation.nix>
+          ];
+
+          boot.loader.grub = {
+            enable = true;
+            device = "/dev/vda";
+            forceInstall = true;
+          };
+
+          documentation.enable = false;
+
+          environment.systemPackages = [
+            (pkgs.writeShellScriptBin "parent" "")
+          ];
+
+          specialisation.foo = {
+            inheritParentConfig = true;
+
+            configuration = { ... }: {
+              environment.systemPackages = [
+                (pkgs.writeShellScriptBin "foo" "")
+              ];
+            };
+          };
+
+          specialisation.bar = {
+            inheritParentConfig = true;
+
+            configuration = { ... }: {
+              environment.systemPackages = [
+                (pkgs.writeShellScriptBin "bar" "")
+              ];
+            };
+          };
+        }
+      '';
+
+    in
+    ''
+      machine.start()
+      machine.succeed("udevadm settle")
+      machine.wait_for_unit("multi-user.target")
+
+      machine.succeed("nixos-generate-config")
+      machine.copy_from_host(
+          "${configFile}",
+          "/etc/nixos/configuration.nix",
+      )
+
+      with subtest("Switch to the base system"):
+          machine.succeed("nixos-rebuild switch")
+          machine.succeed("parent")
+          machine.fail("foo")
+          machine.fail("bar")
+
+      with subtest("Switch from base system into a specialization"):
+          machine.succeed("nixos-rebuild switch --specialisation foo")
+          machine.succeed("parent")
+          machine.succeed("foo")
+          machine.fail("bar")
+
+      with subtest("Switch from specialization into another specialization"):
+          machine.succeed("nixos-rebuild switch -c bar")
+          machine.succeed("parent")
+          machine.fail("foo")
+          machine.succeed("bar")
+
+      with subtest("Switch from specialization into the base system"):
+          machine.succeed("nixos-rebuild switch")
+          machine.succeed("parent")
+          machine.fail("foo")
+          machine.fail("bar")
+
+      with subtest("Switch into specialization using `nixos-rebuild test`"):
+          machine.succeed("nixos-rebuild test --specialisation foo")
+          machine.succeed("parent")
+          machine.succeed("foo")
+          machine.fail("bar")
+
+      with subtest("Make sure nonsense command combinations are forbidden"):
+          machine.fail("nixos-rebuild boot --specialisation foo")
+          machine.fail("nixos-rebuild boot -c foo")
+    '';
+})
diff --git a/nixos/tests/extra-python-packages.nix b/nixos/tests/nixos-test-driver/extra-python-packages.nix
index 7a48077cf98..1146bedd996 100644
--- a/nixos/tests/extra-python-packages.nix
+++ b/nixos/tests/nixos-test-driver/extra-python-packages.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ ... }:
+import ../make-test-python.nix ({ ... }:
   {
     name = "extra-python-packages";
 
diff --git a/nixos/tests/nixos-test-driver/node-name.nix b/nixos/tests/nixos-test-driver/node-name.nix
new file mode 100644
index 00000000000..31386813a51
--- /dev/null
+++ b/nixos/tests/nixos-test-driver/node-name.nix
@@ -0,0 +1,33 @@
+{
+  name = "nixos-test-driver.node-name";
+  nodes = {
+    "ok" = { };
+
+    # Valid node name, but not a great host name.
+    "one_two" = { };
+
+    # Valid node name, good host name
+    "a-b" = { };
+
+    # TODO: would be nice to test these eval failures
+    # Not allowed by lib/testing/network.nix (yet?)
+    # "foo.bar" = { };
+    # Not allowed.
+    # "not ok" = { }; # not ok
+  };
+
+  testScript = ''
+    start_all()
+
+    with subtest("python vars exist and machines are reachable through test backdoor"):
+      ok.succeed("true")
+      one_two.succeed("true")
+      a_b.succeed("true")
+
+    with subtest("hostname is derived from the node name"):
+      ok.succeed("hostname | tee /dev/stderr | grep '^ok$'")
+      one_two.succeed("hostname | tee /dev/stderr | grep '^onetwo$'")
+      a_b.succeed("hostname | tee /dev/stderr | grep '^a-b$'")
+
+  '';
+}
diff --git a/nixos/tests/non-default-filesystems.nix b/nixos/tests/non-default-filesystems.nix
index 7fa75aaad72..d4e8bfbc65e 100644
--- a/nixos/tests/non-default-filesystems.nix
+++ b/nixos/tests/non-default-filesystems.nix
@@ -5,9 +5,10 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
   nodes.machine =
     { config, pkgs, lib, ... }:
     let
-      disk = config.virtualisation.bootDevice;
+      disk = config.virtualisation.rootDevice;
     in
     {
+      virtualisation.rootDevice = "/dev/vda";
       virtualisation.useDefaultFilesystems = false;
 
       boot.initrd.availableKernelModules = [ "btrfs" ];
diff --git a/nixos/tests/noto-fonts-cjk-qt-default-weight.nix b/nixos/tests/noto-fonts-cjk-qt-default-weight.nix
new file mode 100644
index 00000000000..678013cf3ab
--- /dev/null
+++ b/nixos/tests/noto-fonts-cjk-qt-default-weight.nix
@@ -0,0 +1,30 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "noto-fonts-cjk-qt";
+  meta.maintainers = with lib.maintainers; [ oxalica ];
+
+  nodes.machine = {
+    imports = [ ./common/x11.nix ];
+    fonts = {
+      enableDefaultFonts = false;
+      fonts = [ pkgs.noto-fonts-cjk-sans ];
+    };
+  };
+
+  testScript =
+    let
+      script = pkgs.writers.writePython3 "qt-default-weight" {
+        libraries = [ pkgs.python3Packages.pyqt6 ];
+      } ''
+        from PyQt6.QtWidgets import QApplication
+        from PyQt6.QtGui import QFont, QRawFont
+
+        app = QApplication([])
+        f = QRawFont.fromFont(QFont("Noto Sans CJK SC", 20))
+
+        assert f.styleName() == "Regular", f.styleName()
+      '';
+    in ''
+      machine.wait_for_x()
+      machine.succeed("${script}")
+    '';
+})
diff --git a/nixos/tests/nscd.nix b/nixos/tests/nscd.nix
index 1922812ef8c..356c6d2e2a5 100644
--- a/nixos/tests/nscd.nix
+++ b/nixos/tests/nscd.nix
@@ -40,12 +40,13 @@ in
     };
 
     specialisation = {
+      withGlibcNscd.configuration = { ... }: {
+        services.nscd.enableNsncd = false;
+      };
       withUnscd.configuration = { ... }: {
+        services.nscd.enableNsncd = false;
         services.nscd.package = pkgs.unscd;
       };
-      withNsncd.configuration = { ... }: {
-        services.nscd.enableNsncd = true;
-      };
     };
   };
 
@@ -118,6 +119,14 @@ in
       test_host_lookups()
       test_nss_myhostname()
 
+      with subtest("glibc-nscd"):
+          machine.succeed('${specialisations}/withGlibcNscd/bin/switch-to-configuration test')
+          machine.wait_for_unit("default.target")
+
+          test_dynamic_user()
+          test_host_lookups()
+          test_nss_myhostname()
+
       with subtest("unscd"):
           machine.succeed('${specialisations}/withUnscd/bin/switch-to-configuration test')
           machine.wait_for_unit("default.target")
@@ -129,13 +138,5 @@ in
 
           # known to fail, unscd doesn't load external NSS modules
           # test_nss_myhostname()
-
-      with subtest("nsncd"):
-          machine.succeed('${specialisations}/withNsncd/bin/switch-to-configuration test')
-          machine.wait_for_unit("default.target")
-
-          test_dynamic_user()
-          test_host_lookups()
-          test_nss_myhostname()
     '';
 })
diff --git a/nixos/tests/ntfy-sh.nix b/nixos/tests/ntfy-sh.nix
index 9a36fcdf392..4492fb44ced 100644
--- a/nixos/tests/ntfy-sh.nix
+++ b/nixos/tests/ntfy-sh.nix
@@ -12,6 +12,8 @@ import ./make-test-python.nix {
 
     machine.wait_for_unit("multi-user.target")
 
+    machine.wait_for_open_port(80)
+
     machine.succeed(f"curl -d '{msg}' localhost:80/test")
 
     notif = json.loads(machine.succeed("curl -s localhost:80/test/json?poll=1"))
diff --git a/nixos/tests/octoprint.nix b/nixos/tests/octoprint.nix
new file mode 100644
index 00000000000..15a2d677d4c
--- /dev/null
+++ b/nixos/tests/octoprint.nix
@@ -0,0 +1,61 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+let
+  apikey = "testapikey";
+in
+{
+  name = "octoprint";
+  meta.maintainers = with lib.maintainers; [ gador ];
+
+  nodes.machine = { pkgs, ... }: {
+    environment.systemPackages = with pkgs; [ jq ];
+    services.octoprint = {
+      enable = true;
+      extraConfig = {
+        server = {
+          firstRun = false;
+        };
+        api = {
+          enabled = true;
+          key = apikey;
+        };
+        plugins = {
+          # these need internet access and pollute the output with connection failed errors
+          _disabled = [ "softwareupdate" "announcements" "pluginmanager" ];
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    import json
+
+    @polling_condition
+    def octoprint_running():
+        machine.succeed("pgrep octoprint")
+
+    with subtest("Wait for octoprint service to start"):
+        machine.wait_for_unit("octoprint.service")
+        machine.wait_until_succeeds("pgrep octoprint")
+
+    with subtest("Wait for final boot"):
+        # this appears whe octoprint is almost finished starting
+        machine.wait_for_file("/var/lib/octoprint/uploads")
+
+    # octoprint takes some time to start. This makes sure we'll retry just in case it takes longer
+    # retry-all-errors in necessary, since octoprint will report a 404 error when not yet ready
+    curl_cmd = "curl --retry-all-errors --connect-timeout 5 --max-time 10 --retry 5 --retry-delay 0 \
+                --retry-max-time 40 -X GET --header 'X-API-Key: ${apikey}' "
+
+    # used to fail early, in case octoprint first starts and then crashes
+    with octoprint_running: # type: ignore[union-attr]
+        with subtest("Check for web interface"):
+            machine.wait_until_succeeds("curl -s localhost:5000")
+
+        with subtest("Check API"):
+            version = json.loads(machine.succeed(curl_cmd + "localhost:5000/api/version"))
+            server = json.loads(machine.succeed(curl_cmd + "localhost:5000/api/server"))
+            assert version["server"] == str("${pkgs.octoprint.version}")
+            assert server["safemode"] == None
+  '';
+})
diff --git a/nixos/tests/openldap.nix b/nixos/tests/openldap.nix
index 075bb5d1f64..47d6a91843f 100644
--- a/nixos/tests/openldap.nix
+++ b/nixos/tests/openldap.nix
@@ -118,7 +118,7 @@ in {
     };
   };
   testScript = { nodes, ... }: let
-    specializations = "${nodes.machine.config.system.build.toplevel}/specialisation";
+    specializations = "${nodes.machine.system.build.toplevel}/specialisation";
     changeRootPw = ''
       dn: olcDatabase={1}mdb,cn=config
       changetype: modify
diff --git a/nixos/tests/opensearch.nix b/nixos/tests/opensearch.nix
new file mode 100644
index 00000000000..c0caf950cb9
--- /dev/null
+++ b/nixos/tests/opensearch.nix
@@ -0,0 +1,52 @@
+let
+  opensearchTest =
+    import ./make-test-python.nix (
+      { pkgs, lib, extraSettings ? {} }: {
+        name = "opensearch";
+        meta.maintainers = with pkgs.lib.maintainers; [ shyim ];
+
+        nodes.machine = lib.mkMerge [
+          {
+            virtualisation.memorySize = 2048;
+            services.opensearch.enable = true;
+          }
+          extraSettings
+        ];
+
+        testScript = ''
+          machine.start()
+          machine.wait_for_unit("opensearch.service")
+          machine.wait_for_open_port(9200)
+
+          machine.succeed(
+              "curl --fail localhost:9200"
+          )
+        '';
+      });
+in
+{
+  opensearch = opensearchTest {};
+  opensearchCustomPathAndUser = opensearchTest {
+    extraSettings = {
+      services.opensearch.dataDir = "/var/opensearch_test";
+      services.opensearch.user = "open_search";
+      services.opensearch.group = "open_search";
+      system.activationScripts.createDirectory = {
+        text = ''
+          mkdir -p "/var/opensearch_test"
+          chown open_search:open_search /var/opensearch_test
+          chmod 0700 /var/opensearch_test
+        '';
+        deps = [ "users" "groups" ];
+      };
+      users = {
+        groups.open_search = {};
+        users.open_search = {
+          description = "OpenSearch daemon user";
+          group = "open_search";
+          isSystemUser = true;
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/tests/pam/test_chfn.py b/nixos/tests/pam/test_chfn.py
index b108a9423ca..a48438b8d30 100644
--- a/nixos/tests/pam/test_chfn.py
+++ b/nixos/tests/pam/test_chfn.py
@@ -8,7 +8,7 @@ expected_lines = {
     "auth sufficient pam_rootok.so",
     "auth sufficient pam_unix.so   likeauth try_first_pass",
     "password sufficient @@pam_krb5@@/lib/security/pam_krb5.so use_first_pass",
-    "password sufficient pam_unix.so nullok sha512",
+    "password sufficient pam_unix.so nullok yescrypt",
     "session optional @@pam_krb5@@/lib/security/pam_krb5.so",
     "session required pam_env.so conffile=/etc/pam/environment readenv=0",
     "session required pam_unix.so",
diff --git a/nixos/tests/pantheon.nix b/nixos/tests/pantheon.nix
index 52f85f5c07d..0b920c7a6d5 100644
--- a/nixos/tests/pantheon.nix
+++ b/nixos/tests/pantheon.nix
@@ -15,13 +15,14 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
     services.xserver.enable = true;
     services.xserver.desktopManager.pantheon.enable = true;
 
+    environment.systemPackages = [ pkgs.xdotool ];
   };
 
   enableOCR = true;
 
   testScript = { nodes, ... }: let
-    user = nodes.machine.config.users.users.alice;
-    bob = nodes.machine.config.users.users.bob;
+    user = nodes.machine.users.users.alice;
+    bob = nodes.machine.users.users.bob;
   in ''
     machine.wait_for_unit("display-manager.service")
 
@@ -29,6 +30,10 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
         machine.wait_for_text("${user.description}")
         # OCR was struggling with this one.
         # machine.wait_for_text("${bob.description}")
+        # Ensure the password box is focused by clicking it.
+        # Workaround for https://github.com/NixOS/nixpkgs/issues/211366.
+        machine.succeed("XAUTHORITY=/var/lib/lightdm/.Xauthority DISPLAY=:0 xdotool mousemove 512 505 click 1")
+        machine.sleep(2)
         machine.screenshot("elementary_greeter_lightdm")
 
     with subtest("Login with elementary-greeter"):
@@ -40,7 +45,6 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
     with subtest("Check that logging in has given the user ownership of devices"):
         machine.succeed("getfacl -p /dev/snd/timer | grep -q ${user.name}")
 
-    # TODO: DBus API could eliminate this? Pantheon uses Bamf.
     with subtest("Check if pantheon session components actually start"):
         machine.wait_until_succeeds("pgrep gala")
         machine.wait_for_window("gala")
@@ -49,6 +53,12 @@ import ./make-test-python.nix ({ pkgs, lib, ...} :
         machine.wait_until_succeeds("pgrep plank")
         machine.wait_for_window("plank")
 
+    with subtest("Open system settings"):
+        machine.execute("su - ${user.name} -c 'DISPLAY=:0 io.elementary.switchboard >&2 &'")
+        # Wait for all plugins to be loaded before we check if the window is still there.
+        machine.sleep(5)
+        machine.wait_for_window("io.elementary.switchboard")
+
     with subtest("Open elementary terminal"):
         machine.execute("su - ${user.name} -c 'DISPLAY=:0 io.elementary.terminal >&2 &'")
         machine.wait_for_window("io.elementary.terminal")
diff --git a/nixos/tests/paperless.nix b/nixos/tests/paperless.nix
index b97834835c2..7f36de4c29b 100644
--- a/nixos/tests/paperless.nix
+++ b/nixos/tests/paperless.nix
@@ -26,6 +26,10 @@ import ./make-test-python.nix ({ lib, ... }: {
         # Wait until server accepts connections
         machine.wait_until_succeeds("curl -fs localhost:28981")
 
+    # Required for consuming documents via the web interface
+    with subtest("Task-queue gets ready"):
+        machine.wait_for_unit("paperless-task-queue.service")
+
     with subtest("Add a document via the web interface"):
         machine.succeed(
             "convert -size 400x40 xc:white -font 'DejaVu-Sans' -pointsize 20 -fill black "
diff --git a/nixos/tests/parsedmarc/default.nix b/nixos/tests/parsedmarc/default.nix
index 50b977723e9..1feadcb7f39 100644
--- a/nixos/tests/parsedmarc/default.nix
+++ b/nixos/tests/parsedmarc/default.nix
@@ -84,8 +84,6 @@ in
             };
           };
 
-          services.elasticsearch.package = pkgs.elasticsearch-oss;
-
           environment.systemPackages = [
             (sendEmail "dmarc@localhost")
             pkgs.jq
@@ -155,12 +153,9 @@ in
                   ssl = true;
                   user = "alice";
                   password = "${pkgs.writeText "imap-password" "foobar"}";
-                  watch = true;
                 };
               };
 
-              services.elasticsearch.package = pkgs.elasticsearch-oss;
-
               environment.systemPackages = [
                 pkgs.jq
               ];
diff --git a/nixos/tests/pass-secret-service.nix b/nixos/tests/pass-secret-service.nix
index a85a508bfe1..e0dddf0ad29 100644
--- a/nixos/tests/pass-secret-service.nix
+++ b/nixos/tests/pass-secret-service.nix
@@ -1,6 +1,6 @@
 import ./make-test-python.nix ({ pkgs, lib, ... }: {
   name = "pass-secret-service";
-  meta.maintainers = with lib; [ aidalgol ];
+  meta.maintainers = [ lib.maintainers.aidalgol ];
 
   nodes.machine = { nodes, pkgs, ... }:
     {
diff --git a/nixos/tests/peroxide.nix b/nixos/tests/peroxide.nix
new file mode 100644
index 00000000000..12e19648416
--- /dev/null
+++ b/nixos/tests/peroxide.nix
@@ -0,0 +1,16 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "peroxide";
+  meta.maintainers = with lib.maintainers; [ aidalgol ];
+
+  nodes.machine =
+    { config, pkgs, ... }: {
+      networking.hostName = "nixos";
+      services.peroxide.enable = true;
+    };
+
+  testScript = ''
+    machine.wait_for_unit("peroxide.service")
+    machine.wait_for_open_port(1143) # IMAP
+    machine.wait_for_open_port(1025) # SMTP
+  '';
+})
diff --git a/nixos/tests/pgadmin4-standalone.nix b/nixos/tests/pgadmin4-standalone.nix
deleted file mode 100644
index 5aa17fcb5bb..00000000000
--- a/nixos/tests/pgadmin4-standalone.nix
+++ /dev/null
@@ -1,43 +0,0 @@
-import ./make-test-python.nix ({ pkgs, lib, ... }:
-  # This is separate 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
index 2a2b5aaa284..6a9ce6ceae2 100644
--- a/nixos/tests/pgadmin4.nix
+++ b/nixos/tests/pgadmin4.nix
@@ -1,133 +1,57 @@
-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;
-        })
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+{
+  name = "pgadmin4";
+  meta.maintainers = with lib.maintainers; [ mkg20001 gador ];
+
+  nodes.machine = { pkgs, ... }: {
+
+    imports = [ ./common/user-account.nix ];
+
+    environment.systemPackages = with pkgs; [
+      curl
+      pgadmin4-desktopmode
+    ];
+
+    services.postgresql = {
+      enable = true;
+      authentication = ''
+        host    all             all             localhost               trust
+      '';
+      ensureUsers = [
+        {
+          name = "postgres";
+          ensurePermissions = {
+            "DATABASE \"postgres\"" = "ALL PRIVILEGES";
+          };
+        }
       ];
+    };
 
-      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";
-            };
-          }
-        ];
-      };
+    services.pgadmin = {
+      port = 5051;
+      enable = true;
+      initialEmail = "bruh@localhost.de";
+      initialPasswordFile = pkgs.writeText "pw" "bruh2012!";
     };
+  };
 
-    testScript = ''
+  testScript = ''
+    with subtest("Check pgadmin module"):
       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 or kerberos authentication
-      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,browser.tests.test_kerberos_with_mocking'
-          )
-
-      # 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'
-          )
-
-      with subtest("run resql test"):
-         machine.succeed(
-              'cd ${pgadmin4SrcDir}/pgadmin4-${pkgs.pgadmin4.version}/web \
-              && python regression/runtests.py --pkg resql'
-         )
-    '';
-  })
+      machine.wait_for_unit("pgadmin")
+      machine.wait_until_succeeds("curl -s localhost:5051")
+      machine.wait_until_succeeds("curl -s localhost:5051/login | grep \"<title>pgAdmin 4</title>\" > /dev/null")
+
+    # pgadmin4 module saves the configuration to /etc/pgadmin/config_system.py
+    # pgadmin4-desktopmode tries to read that as well. This normally fails with a PermissionError, as the config file
+    # is owned by the user of the pgadmin module. With the check-system-config-dir.patch this will just throw a warning
+    # but will continue and not read the file.
+    # If we run pgadmin4-desktopmode as root (something one really shouldn't do), it can read the config file and fail,
+    # because of the wrong config for desktopmode.
+    with subtest("Check pgadmin standalone desktop mode"):
+      machine.execute("sudo -u alice pgadmin4 >&2 &", timeout=60)
+      machine.wait_until_succeeds("curl -s localhost:5050")
+      machine.wait_until_succeeds("curl -s localhost:5050/browser/ | grep \"<title>pgAdmin 4</title>\" > /dev/null")
+  '';
+})
diff --git a/nixos/tests/phosh.nix b/nixos/tests/phosh.nix
index 25bf4848542..78d6da31bee 100644
--- a/nixos/tests/phosh.nix
+++ b/nixos/tests/phosh.nix
@@ -3,7 +3,7 @@ import ./make-test-python.nix ({ pkgs, ...}: let
 in {
   name = "phosh";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ zhaofengli ];
+    maintainers = [ tomfitzhenry zhaofengli ];
   };
 
   nodes = {
diff --git a/nixos/tests/photoprism.nix b/nixos/tests/photoprism.nix
new file mode 100644
index 00000000000..a77ab59f5c9
--- /dev/null
+++ b/nixos/tests/photoprism.nix
@@ -0,0 +1,23 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "photoprism";
+  meta.maintainers = with lib.maintainers; [ stunkymonkey ];
+
+  nodes.machine = { pkgs, ... }: {
+    services.photoprism = {
+      enable = true;
+      port = 8080;
+      originalsPath = "/media/photos/";
+      passwordFile = pkgs.writeText "password" "secret";
+    };
+    environment.extraInit = ''
+      mkdir -p /media/photos
+    '';
+  };
+
+  testScript = ''
+    machine.wait_for_unit("multi-user.target")
+    machine.wait_for_open_port(8080)
+    response = machine.succeed("curl -vvv -s -H 'Host: photoprism' http://127.0.0.1:8080/library/login")
+    assert '<title>PhotoPrism</title>' in response, "Login page didn't load successfully"
+  '';
+})
diff --git a/nixos/tests/pleroma.nix b/nixos/tests/pleroma.nix
index 8998716243a..4f1aef85414 100644
--- a/nixos/tests/pleroma.nix
+++ b/nixos/tests/pleroma.nix
@@ -170,8 +170,8 @@ import ./make-test-python.nix ({ pkgs, ... }:
   '';
 
   hosts = nodes: ''
-    ${nodes.pleroma.config.networking.primaryIPAddress} pleroma.nixos.test
-    ${nodes.client.config.networking.primaryIPAddress} client.nixos.test
+    ${nodes.pleroma.networking.primaryIPAddress} pleroma.nixos.test
+    ${nodes.client.networking.primaryIPAddress} client.nixos.test
   '';
   in {
   name = "pleroma";
diff --git a/nixos/tests/podman/default.nix b/nixos/tests/podman/default.nix
index 106ba2057d0..0e1f420f2a7 100644
--- a/nixos/tests/podman/default.nix
+++ b/nixos/tests/podman/default.nix
@@ -6,13 +6,27 @@ import ../make-test-python.nix (
     };
 
     nodes = {
-      podman = { pkgs, ... }: {
+      rootful = { pkgs, ... }: {
+        virtualisation.podman.enable = true;
+
+        # hack to ensure that podman built with and without zfs in extraPackages is cached
+        boot.supportedFilesystems = [ "zfs" ];
+        networking.hostId = "00000000";
+      };
+      rootless = { pkgs, ... }: {
         virtualisation.podman.enable = true;
 
         users.users.alice = {
           isNormalUser = true;
         };
       };
+      dns = { pkgs, ... }: {
+        virtualisation.podman.enable = true;
+
+        virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
+
+        networking.firewall.allowedUDPPorts = [ 53 ];
+      };
       docker = { pkgs, ... }: {
         virtualisation.podman.enable = true;
 
@@ -42,89 +56,114 @@ import ../make-test-python.nix (
           return f"su {user} -l -c {cmd}"
 
 
-      podman.wait_for_unit("sockets.target")
+      rootful.wait_for_unit("sockets.target")
+      rootless.wait_for_unit("sockets.target")
+      dns.wait_for_unit("sockets.target")
       docker.wait_for_unit("sockets.target")
       start_all()
 
       with subtest("Run container as root with runc"):
-          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
-          podman.succeed(
+          rootful.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
+          rootful.succeed(
               "podman run --runtime=runc -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
           )
-          podman.succeed("podman ps | grep sleeping")
-          podman.succeed("podman stop sleeping")
-          podman.succeed("podman rm sleeping")
+          rootful.succeed("podman ps | grep sleeping")
+          rootful.succeed("podman stop sleeping")
+          rootful.succeed("podman rm sleeping")
 
       with subtest("Run container as root with crun"):
-          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
-          podman.succeed(
+          rootful.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
+          rootful.succeed(
               "podman run --runtime=crun -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
           )
-          podman.succeed("podman ps | grep sleeping")
-          podman.succeed("podman stop sleeping")
-          podman.succeed("podman rm sleeping")
+          rootful.succeed("podman ps | grep sleeping")
+          rootful.succeed("podman stop sleeping")
+          rootful.succeed("podman rm sleeping")
 
       with subtest("Run container as root with the default backend"):
-          podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
-          podman.succeed(
+          rootful.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
+          rootful.succeed(
               "podman run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
           )
-          podman.succeed("podman ps | grep sleeping")
-          podman.succeed("podman stop sleeping")
-          podman.succeed("podman rm sleeping")
+          rootful.succeed("podman ps | grep sleeping")
+          rootful.succeed("podman stop sleeping")
+          rootful.succeed("podman rm sleeping")
 
       # start systemd session for rootless
-      podman.succeed("loginctl enable-linger alice")
-      podman.succeed(su_cmd("whoami"))
-      podman.sleep(1)
+      rootless.succeed("loginctl enable-linger alice")
+      rootless.succeed(su_cmd("whoami"))
+      rootless.sleep(1)
 
       with subtest("Run container rootless with runc"):
-          podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
-          podman.succeed(
+          rootless.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
+          rootless.succeed(
               su_cmd(
                   "podman run --runtime=runc -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
               )
           )
-          podman.succeed(su_cmd("podman ps | grep sleeping"))
-          podman.succeed(su_cmd("podman stop sleeping"))
-          podman.succeed(su_cmd("podman rm sleeping"))
+          rootless.succeed(su_cmd("podman ps | grep sleeping"))
+          rootless.succeed(su_cmd("podman stop sleeping"))
+          rootless.succeed(su_cmd("podman rm sleeping"))
 
       with subtest("Run container rootless with crun"):
-          podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
-          podman.succeed(
+          rootless.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
+          rootless.succeed(
               su_cmd(
                   "podman run --runtime=crun -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
               )
           )
-          podman.succeed(su_cmd("podman ps | grep sleeping"))
-          podman.succeed(su_cmd("podman stop sleeping"))
-          podman.succeed(su_cmd("podman rm sleeping"))
+          rootless.succeed(su_cmd("podman ps | grep sleeping"))
+          rootless.succeed(su_cmd("podman stop sleeping"))
+          rootless.succeed(su_cmd("podman rm sleeping"))
 
       with subtest("Run container rootless with the default backend"):
-          podman.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
-          podman.succeed(
+          rootless.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
+          rootless.succeed(
               su_cmd(
                   "podman run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg /bin/sleep 10"
               )
           )
-          podman.succeed(su_cmd("podman ps | grep sleeping"))
-          podman.succeed(su_cmd("podman stop sleeping"))
-          podman.succeed(su_cmd("podman rm sleeping"))
+          rootless.succeed(su_cmd("podman ps | grep sleeping"))
+          rootless.succeed(su_cmd("podman stop sleeping"))
+          rootless.succeed(su_cmd("podman rm sleeping"))
+
+      with subtest("rootlessport"):
+          rootless.succeed(su_cmd("tar cv --files-from /dev/null | podman import - scratchimg"))
+          rootless.succeed(
+              su_cmd(
+                  "podman run -d -p 9000:8888 --name=rootlessport -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin -w ${pkgs.writeTextDir "index.html" "<h1>Testing</h1>"} scratchimg ${pkgs.python3}/bin/python -m http.server 8888"
+              )
+          )
+          rootless.succeed(su_cmd("podman ps | grep rootlessport"))
+          rootless.wait_until_succeeds(su_cmd("${pkgs.curl}/bin/curl localhost:9000 | grep Testing"))
+          rootless.succeed(su_cmd("podman stop rootlessport"))
+          rootless.succeed(su_cmd("podman rm rootlessport"))
 
       with subtest("Run container with init"):
-          podman.succeed(
+          rootful.succeed(
               "tar cv -C ${pkgs.pkgsStatic.busybox} . | podman import - busybox"
           )
-          pid = podman.succeed("podman run --rm busybox readlink /proc/self").strip()
+          pid = rootful.succeed("podman run --rm busybox readlink /proc/self").strip()
           assert pid == "1"
-          pid = podman.succeed("podman run --rm --init busybox readlink /proc/self").strip()
+          pid = rootful.succeed("podman run --rm --init busybox readlink /proc/self").strip()
           assert pid == "2"
 
+      with subtest("aardvark-dns"):
+          dns.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
+          dns.succeed(
+              "podman run -d --name=webserver -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin -w ${pkgs.writeTextDir "index.html" "<h1>Testing</h1>"} scratchimg ${pkgs.python3}/bin/python -m http.server 8000"
+          )
+          dns.succeed("podman ps | grep webserver")
+          dns.wait_until_succeeds(
+              "podman run --rm --name=client -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg ${pkgs.curl}/bin/curl http://webserver:8000 | grep Testing"
+          )
+          dns.succeed("podman stop webserver")
+          dns.succeed("podman rm webserver")
+
       with subtest("A podman member can use the docker cli"):
           docker.succeed(su_cmd("docker version"))
 
       with subtest("Run container via docker cli"):
-          docker.succeed("docker network create default")
           docker.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
           docker.succeed(
             "docker run -d --name=sleeping -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin localhost/scratchimg /bin/sleep 10"
@@ -133,7 +172,6 @@ import ../make-test-python.nix (
           docker.succeed("podman ps | grep sleeping")
           docker.succeed("docker stop sleeping")
           docker.succeed("docker rm sleeping")
-          docker.succeed("docker network rm default")
 
       with subtest("A podman non-member can not use the docker cli"):
           docker.fail(su_cmd("docker version", user="mallory"))
diff --git a/nixos/tests/podman/dnsname.nix b/nixos/tests/podman/dnsname.nix
deleted file mode 100644
index 3768ae79e06..00000000000
--- a/nixos/tests/podman/dnsname.nix
+++ /dev/null
@@ -1,42 +0,0 @@
-import ../make-test-python.nix (
-  { pkgs, lib, ... }:
-  let
-    inherit (pkgs) writeTextDir python3 curl;
-    webroot = writeTextDir "index.html" "<h1>Hi</h1>";
-  in
-  {
-    name = "podman-dnsname";
-    meta = {
-      maintainers = with lib.maintainers; [ roberth ] ++ lib.teams.podman.members;
-    };
-
-    nodes = {
-      podman = { pkgs, ... }: {
-        virtualisation.podman.enable = true;
-        virtualisation.podman.defaultNetwork.dnsname.enable = true;
-      };
-    };
-
-    testScript = ''
-      podman.wait_for_unit("sockets.target")
-
-      with subtest("DNS works"): # also tests inter-container tcp routing
-        podman.succeed("tar cv --files-from /dev/null | podman import - scratchimg")
-        podman.succeed(
-          "podman run -d --name=webserver -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin -w ${webroot} scratchimg ${python3}/bin/python -m http.server 8000"
-        )
-        podman.succeed("podman ps | grep webserver")
-        podman.succeed("""
-          for i in `seq 0 120`; do
-            podman run --rm --name=client -v /nix/store:/nix/store -v /run/current-system/sw/bin:/bin scratchimg ${curl}/bin/curl http://webserver:8000 >/dev/console \
-              && exit 0
-            sleep 0.5
-          done
-          exit 1
-        """)
-        podman.succeed("podman stop webserver")
-        podman.succeed("podman rm webserver")
-
-    '';
-  }
-)
diff --git a/nixos/tests/podman/tls-ghostunnel.nix b/nixos/tests/podman/tls-ghostunnel.nix
index 268a55701cc..52c31dc21f1 100644
--- a/nixos/tests/podman/tls-ghostunnel.nix
+++ b/nixos/tests/podman/tls-ghostunnel.nix
@@ -113,9 +113,6 @@ import ../make-test-python.nix (
       podman.wait_for_unit("sockets.target")
       podman.wait_for_unit("ghostunnel-server-podman-socket.service")
 
-      with subtest("Create default network"):
-          podman.succeed("docker network create default")
-
       with subtest("Root docker cli also works"):
           podman.succeed("docker version")
 
diff --git a/nixos/tests/pomerium.nix b/nixos/tests/pomerium.nix
index 7af82832644..abaf56c518e 100644
--- a/nixos/tests/pomerium.nix
+++ b/nixos/tests/pomerium.nix
@@ -20,6 +20,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
   }; in {
     pomerium = { pkgs, lib, ... }: {
       imports = [ (base "192.168.1.1") ];
+      environment.systemPackages = with pkgs; [ chromium ];
       services.pomerium = {
         enable = true;
         settings = {
@@ -98,5 +99,11 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
         pomerium.succeed(
             "curl -L --resolve login.required:80:127.0.0.1 http://login.required | grep 'hello I am login page'"
         )
+
+    with subtest("ui"):
+        pomerium.succeed(
+          # check for a string that only appears if the UI is displayed correctly
+            "chromium --no-sandbox --headless --disable-gpu --dump-dom --host-resolver-rules='MAP login.required 127.0.0.1:80' http://login.required/.pomerium | grep 'contact your administrator'"
+        )
   '';
 })
diff --git a/nixos/tests/postgresql-jit.nix b/nixos/tests/postgresql-jit.nix
new file mode 100644
index 00000000000..baf26b8da2b
--- /dev/null
+++ b/nixos/tests/postgresql-jit.nix
@@ -0,0 +1,48 @@
+{ system ? builtins.currentSystem
+, config ? {}
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+
+let
+  inherit (pkgs) lib;
+  packages = builtins.attrNames (import ../../pkgs/servers/sql/postgresql pkgs);
+
+  mkJitTest = packageName: makeTest {
+    name = "${packageName}";
+    meta.maintainers = with lib.maintainers; [ ma27 ];
+    nodes.machine = { pkgs, lib, ... }: {
+      services.postgresql = {
+        enable = true;
+        enableJIT = true;
+        package = pkgs.${packageName};
+        initialScript = pkgs.writeText "init.sql" ''
+          create table demo (id int);
+          insert into demo (id) select generate_series(1, 5);
+        '';
+      };
+    };
+    testScript = ''
+      machine.start()
+      machine.wait_for_unit("postgresql.service")
+
+      with subtest("JIT is enabled"):
+          machine.succeed("sudo -u postgres psql <<<'show jit;' | grep 'on'")
+
+      with subtest("Test JIT works fine"):
+          output = machine.succeed(
+              "cat ${pkgs.writeText "test.sql" ''
+                set jit_above_cost = 1;
+                EXPLAIN ANALYZE SELECT CONCAT('jit result = ', SUM(id)) FROM demo;
+                SELECT CONCAT('jit result = ', SUM(id)) from demo;
+              ''} | sudo -u postgres psql"
+          )
+          assert "JIT:" in output
+          assert "jit result = 15" in output
+
+      machine.shutdown()
+    '';
+  };
+in
+lib.genAttrs packages mkJitTest
diff --git a/nixos/tests/postgresql-wal-receiver.nix b/nixos/tests/postgresql-wal-receiver.nix
index ae2708546f5..b0bd7711dbc 100644
--- a/nixos/tests/postgresql-wal-receiver.nix
+++ b/nixos/tests/postgresql-wal-receiver.nix
@@ -116,4 +116,4 @@ let
     };
 
 # Maps the generic function over all attributes of PostgreSQL packages
-in builtins.listToAttrs (map makePostgresqlWalReceiverTest (builtins.attrNames (import ../../pkgs/servers/sql/postgresql { })))
+in builtins.listToAttrs (map makePostgresqlWalReceiverTest (builtins.attrNames (import ../../pkgs/servers/sql/postgresql pkgs)))
diff --git a/nixos/tests/postgresql.nix b/nixos/tests/postgresql.nix
index 7e0a82c3882..b44849e0a14 100644
--- a/nixos/tests/postgresql.nix
+++ b/nixos/tests/postgresql.nix
@@ -137,7 +137,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 278e9471183..c887cde4b82 100644
--- a/nixos/tests/power-profiles-daemon.nix
+++ b/nixos/tests/power-profiles-daemon.nix
@@ -6,6 +6,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
     maintainers = [ mvnetbiz ];
   };
   nodes.machine = { pkgs, ... }: {
+    security.polkit.enable = true;
     services.power-profiles-daemon.enable = true;
     environment.systemPackages = [ pkgs.glib ];
   };
diff --git a/nixos/tests/pppd.nix b/nixos/tests/pppd.nix
index e714a6c21a6..d599f918036 100644
--- a/nixos/tests/pppd.nix
+++ b/nixos/tests/pppd.nix
@@ -28,7 +28,7 @@ import ./make-test-python.nix (
             "ppp/pppoe-server-options".text = ''
               lcp-echo-interval 10
               lcp-echo-failure 2
-              plugin rp-pppoe.so
+              plugin pppoe.so
               require-chap
               nobsdcomp
               noccp
@@ -43,7 +43,7 @@ import ./make-test-python.nix (
           enable = true;
           peers.test = {
             config = ''
-              plugin rp-pppoe.so eth1
+              plugin pppoe.so eth1
               name "flynn"
               noipdefault
               persist
diff --git a/nixos/tests/predictable-interface-names.nix b/nixos/tests/predictable-interface-names.nix
index 08773120bc1..42183625c7c 100644
--- a/nixos/tests/predictable-interface-names.nix
+++ b/nixos/tests/predictable-interface-names.nix
@@ -8,25 +8,48 @@ let
   testCombinations = pkgs.lib.cartesianProductOfSets {
     predictable = [true false];
     withNetworkd = [true false];
+    systemdStage1 = [true false];
   };
-in pkgs.lib.listToAttrs (builtins.map ({ predictable, withNetworkd }: {
+in pkgs.lib.listToAttrs (builtins.map ({ predictable, withNetworkd, systemdStage1 }: {
   name = pkgs.lib.optionalString (!predictable) "un" + "predictable"
-       + pkgs.lib.optionalString withNetworkd "Networkd";
+       + pkgs.lib.optionalString withNetworkd "Networkd"
+       + pkgs.lib.optionalString systemdStage1 "SystemdStage1";
   value = makeTest {
-    name = "${if predictable then "" else "un"}predictableInterfaceNames${if withNetworkd then "-with-networkd" else ""}";
+    name = pkgs.lib.optionalString (!predictable) "un" + "predictableInterfaceNames"
+         + pkgs.lib.optionalString withNetworkd "-with-networkd"
+         + pkgs.lib.optionalString systemdStage1 "-systemd-stage-1";
     meta = {};
 
-    nodes.machine = { lib, ... }: {
+    nodes.machine = { lib, ... }: let
+      script = ''
+        ip link
+        if ${lib.optionalString predictable "!"} ip link show eth0; then
+          echo Success
+        else
+          exit 1
+        fi
+      '';
+    in {
       networking.usePredictableInterfaceNames = lib.mkForce predictable;
       networking.useNetworkd = withNetworkd;
       networking.dhcpcd.enable = !withNetworkd;
       networking.useDHCP = !withNetworkd;
 
       # Check if predictable interface names are working in stage-1
-      boot.initrd.postDeviceCommands = ''
-        ip link
-        ip link show eth0 ${if predictable then "&&" else "||"} exit 1
-      '';
+      boot.initrd.postDeviceCommands = script;
+
+      boot.initrd.systemd = lib.mkIf systemdStage1 {
+        enable = true;
+        initrdBin = [ pkgs.iproute2 ];
+        services.systemd-udev-settle.wantedBy = ["initrd.target"];
+        services.check-interfaces = {
+          requiredBy = ["initrd.target"];
+          after = ["systemd-udev-settle.service"];
+          serviceConfig.Type = "oneshot";
+          path = [ pkgs.iproute2 ];
+          inherit script;
+        };
+      };
     };
 
     testScript = ''
diff --git a/nixos/tests/printing.nix b/nixos/tests/printing.nix
index cfebe232d92..7df042e72e9 100644
--- a/nixos/tests/printing.nix
+++ b/nixos/tests/printing.nix
@@ -1,19 +1,31 @@
 # Test printing via CUPS.
 
-import ./make-test-python.nix ({pkgs, ... }:
-let
-  printingServer = startWhenNeeded: {
-    services.printing.enable = true;
-    services.printing.stateless = true;
-    services.printing.startWhenNeeded = startWhenNeeded;
-    services.printing.listenAddresses = [ "*:631" ];
-    services.printing.defaultShared = true;
-    services.printing.extraConf = ''
-      <Location />
-        Order allow,deny
-        Allow from all
-      </Location>
-    '';
+import ./make-test-python.nix (
+{ pkgs
+, socket ? true # whether to use socket activation
+, ...
+}:
+
+{
+  name = "printing";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ domenkozar eelco matthewbauer ];
+  };
+
+  nodes.server = { ... }: {
+    services.printing = {
+      enable = true;
+      stateless = true;
+      startWhenNeeded = socket;
+      listenAddresses = [ "*:631" ];
+      defaultShared = true;
+      extraConf = ''
+        <Location />
+          Order allow,deny
+          Allow from all
+        </Location>
+      '';
+    };
     networking.firewall.allowedTCPPorts = [ 631 ];
     # Add a HP Deskjet printer connected via USB to the server.
     hardware.printers.ensurePrinters = [{
@@ -22,32 +34,19 @@ let
       model = "drv:///sample.drv/deskjet.ppd";
     }];
   };
-  printingClient = startWhenNeeded: {
+
+  nodes.client = { ... }: {
     services.printing.enable = true;
-    services.printing.startWhenNeeded = startWhenNeeded;
+    services.printing.startWhenNeeded = socket;
     # Add printer to the client as well, via IPP.
     hardware.printers.ensurePrinters = [{
       name = "DeskjetRemote";
-      deviceUri = "ipp://${if startWhenNeeded then "socketActivatedServer" else "serviceServer"}/printers/DeskjetLocal";
+      deviceUri = "ipp://server/printers/DeskjetLocal";
       model = "drv:///sample.drv/deskjet.ppd";
     }];
     hardware.printers.ensureDefaultPrinter = "DeskjetRemote";
   };
 
-in {
-  name = "printing";
-  meta = with pkgs.lib.maintainers; {
-    maintainers = [ domenkozar eelco matthewbauer ];
-  };
-
-  nodes = {
-    socketActivatedServer = { ... }: (printingServer true);
-    serviceServer = { ... }: (printingServer false);
-
-    socketActivatedClient = { ... }: (printingClient true);
-    serviceClient = { ... }: (printingClient false);
-  };
-
   testScript = ''
     import os
     import re
@@ -55,75 +54,69 @@ in {
     start_all()
 
     with subtest("Make sure that cups is up on both sides and printers are set up"):
-        serviceServer.wait_for_unit("cups.service")
-        serviceClient.wait_for_unit("cups.service")
-        socketActivatedClient.wait_for_unit("ensure-printers.service")
-
+        server.wait_for_unit("cups.${if socket then "socket" else "service"}")
+        client.wait_for_unit("cups.${if socket then "socket" else "service"}")
+
+    assert "scheduler is running" in client.succeed("lpstat -r")
+
+    with subtest("UNIX socket is used for connections"):
+        assert "/var/run/cups/cups.sock" in client.succeed("lpstat -H")
+
+    with subtest("HTTP server is available too"):
+        client.succeed("curl --fail http://localhost:631/")
+        client.succeed(f"curl --fail http://{server.name}:631/")
+        server.fail(f"curl --fail --connect-timeout 2 http://{client.name}:631/")
+
+    with subtest("LP status checks"):
+        assert "DeskjetRemote accepting requests" in client.succeed("lpstat -a")
+        assert "DeskjetLocal accepting requests" in client.succeed(
+            f"lpstat -h {server.name}:631 -a"
+        )
+        client.succeed("cupsdisable DeskjetRemote")
+        out = client.succeed("lpq")
+        print(out)
+        assert re.search(
+            "DeskjetRemote is not ready.*no entries",
+            client.succeed("lpq"),
+            flags=re.DOTALL,
+        )
+        client.succeed("cupsenable DeskjetRemote")
+        assert re.match(
+            "DeskjetRemote is ready.*no entries", client.succeed("lpq"), flags=re.DOTALL
+        )
+
+    # Test printing various file types.
+    for file in [
+        "${pkgs.groff.doc}/share/doc/*/examples/mom/penguin.pdf",
+        "${pkgs.groff.doc}/share/doc/*/meref.ps",
+        "${pkgs.cups.out}/share/doc/cups/images/cups.png",
+        "${pkgs.pcre.doc}/share/doc/pcre/pcre.txt",
+    ]:
+        file_name = os.path.basename(file)
+        with subtest(f"print {file_name}"):
+            # Print the file on the client.
+            print(client.succeed("lpq"))
+            client.succeed(f"lp {file}")
+            client.wait_until_succeeds(
+                f"lpq; lpq | grep -q -E 'active.*root.*{file_name}'"
+            )
 
-    def test_printing(client, server):
-        assert "scheduler is running" in client.succeed("lpstat -r")
+            # Ensure that a raw PCL file appeared in the server's queue
+            # (showing that the right filters have been applied).  Of
+            # course, since there is no actual USB printer attached, the
+            # file will stay in the queue forever.
+            server.wait_for_file("/var/spool/cups/d*-001")
+            server.wait_until_succeeds(f"lpq -a | grep -q -E '{file_name}'")
 
-        with subtest("UNIX socket is used for connections"):
-            assert "/var/run/cups/cups.sock" in client.succeed("lpstat -H")
-        with subtest("HTTP server is available too"):
-            client.succeed("curl --fail http://localhost:631/")
-            client.succeed(f"curl --fail http://{server.name}:631/")
-            server.fail(f"curl --fail --connect-timeout 2 http://{client.name}:631/")
+            # Delete the job on the client.  It should disappear on the
+            # server as well.
+            client.succeed("lprm")
+            client.wait_until_succeeds("lpq -a | grep -q -E 'no entries'")
 
-        with subtest("LP status checks"):
-            assert "DeskjetRemote accepting requests" in client.succeed("lpstat -a")
-            assert "DeskjetLocal accepting requests" in client.succeed(
-                f"lpstat -h {server.name}:631 -a"
-            )
-            client.succeed("cupsdisable DeskjetRemote")
-            out = client.succeed("lpq")
-            print(out)
-            assert re.search(
-                "DeskjetRemote is not ready.*no entries",
-                client.succeed("lpq"),
-                flags=re.DOTALL,
-            )
-            client.succeed("cupsenable DeskjetRemote")
-            assert re.match(
-                "DeskjetRemote is ready.*no entries", client.succeed("lpq"), flags=re.DOTALL
-            )
+            retry(lambda _: "no entries" in server.succeed("lpq -a"))
 
-        # Test printing various file types.
-        for file in [
-            "${pkgs.groff.doc}/share/doc/*/examples/mom/penguin.pdf",
-            "${pkgs.groff.doc}/share/doc/*/meref.ps",
-            "${pkgs.cups.out}/share/doc/cups/images/cups.png",
-            "${pkgs.pcre.doc}/share/doc/pcre/pcre.txt",
-        ]:
-            file_name = os.path.basename(file)
-            with subtest(f"print {file_name}"):
-                # Print the file on the client.
-                print(client.succeed("lpq"))
-                client.succeed(f"lp {file}")
-                client.wait_until_succeeds(
-                    f"lpq; lpq | grep -q -E 'active.*root.*{file_name}'"
-                )
-
-                # Ensure that a raw PCL file appeared in the server's queue
-                # (showing that the right filters have been applied).  Of
-                # course, since there is no actual USB printer attached, the
-                # file will stay in the queue forever.
-                server.wait_for_file("/var/spool/cups/d*-001")
-                server.wait_until_succeeds(f"lpq -a | grep -q -E '{file_name}'")
-
-                # Delete the job on the client.  It should disappear on the
-                # server as well.
-                client.succeed("lprm")
-                client.wait_until_succeeds("lpq -a | grep -q -E 'no entries'")
-
-                retry(lambda _: "no entries" in server.succeed("lpq -a"))
-
-                # The queue is empty already, so this should be safe.
-                # Otherwise, pairs of "c*"-"d*-001" files might persist.
-                server.execute("rm /var/spool/cups/*")
-
-
-    test_printing(serviceClient, serviceServer)
-    test_printing(socketActivatedClient, socketActivatedServer)
+            # The queue is empty already, so this should be safe.
+            # Otherwise, pairs of "c*"-"d*-001" files might persist.
+            server.execute("rm /var/spool/cups/*")
   '';
 })
diff --git a/nixos/tests/prometheus-exporters.nix b/nixos/tests/prometheus-exporters.nix
index 5f50a3f87d5..82d50da6381 100644
--- a/nixos/tests/prometheus-exporters.nix
+++ b/nixos/tests/prometheus-exporters.nix
@@ -1060,6 +1060,20 @@ let
       '';
     };
 
+    shelly = {
+      exporterConfig = {
+        enable = true;
+        metrics-file = "${pkgs.writeText "test.json" ''{}''}";
+      };
+      exporterTest = ''
+        wait_for_unit("prometheus-shelly-exporter.service")
+        wait_for_open_port(9784)
+        wait_until_succeeds(
+            "curl -sSf 'localhost:9784/metrics'"
+        )
+      '';
+    };
+
     script = {
       exporterConfig = {
         enable = true;
diff --git a/nixos/tests/promscale.nix b/nixos/tests/promscale.nix
new file mode 100644
index 00000000000..d4825b6d7f5
--- /dev/null
+++ b/nixos/tests/promscale.nix
@@ -0,0 +1,60 @@
+# mostly copied from ./timescaledb.nix which was copied from ./postgresql.nix
+# as it seemed unapproriate to test additional extensions for postgresql there.
+
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  postgresql-versions = import ../../pkgs/servers/sql/postgresql pkgs;
+  test-sql = pkgs.writeText "postgresql-test" ''
+    CREATE USER promscale SUPERUSER PASSWORD 'promscale';
+    CREATE DATABASE promscale OWNER promscale;
+  '';
+
+  make-postgresql-test = postgresql-name: postgresql-package: makeTest {
+    name = postgresql-name;
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ anpin ];
+    };
+
+    nodes.machine = { config, pkgs, ... }:
+      {
+        services.postgresql = {
+          enable = true;
+          package = postgresql-package;
+          extraPlugins = with postgresql-package.pkgs; [
+            timescaledb
+            promscale_extension
+          ];
+          settings = { shared_preload_libraries = "timescaledb, promscale"; };
+        };
+        environment.systemPackages = with pkgs; [ promscale ];
+      };
+
+    testScript = ''
+      machine.start()
+      machine.wait_for_unit("postgresql")
+      with subtest("Postgresql with extensions timescaledb and promscale is available just after unit start"):
+          print(machine.succeed("sudo -u postgres psql -f ${test-sql}"))
+          machine.succeed("sudo -u postgres psql promscale -c 'SHOW shared_preload_libraries;' | grep promscale")
+          machine.succeed(
+            "promscale --db.name promscale --db.password promscale --db.user promscale --db.ssl-mode allow --startup.install-extensions --startup.only"
+          )
+      machine.succeed("sudo -u postgres psql promscale -c 'SELECT ps_trace.get_trace_retention_period();' | grep '(1 row)'")
+      machine.shutdown()
+    '';
+  };
+  #version 15 is not supported yet
+  applicablePostgresqlVersions = filterAttrs (_: value: versionAtLeast value.version "12" && !(versionAtLeast value.version "15")) postgresql-versions;
+in
+mapAttrs'
+  (name: package: {
+    inherit name;
+    value = make-postgresql-test name package;
+  })
+  applicablePostgresqlVersions
diff --git a/nixos/tests/pufferpanel.nix b/nixos/tests/pufferpanel.nix
new file mode 100644
index 00000000000..e7b09c13f90
--- /dev/null
+++ b/nixos/tests/pufferpanel.nix
@@ -0,0 +1,74 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "pufferpanel";
+  meta.maintainers = [ lib.maintainers.tie ];
+
+  nodes.machine = { pkgs, ... }: {
+    environment.systemPackages = [ pkgs.pufferpanel ];
+    services.pufferpanel = {
+      enable = true;
+      extraPackages = [ pkgs.netcat ];
+      environment = {
+        PUFFER_PANEL_REGISTRATIONENABLED = "false";
+        PUFFER_PANEL_SETTINGS_COMPANYNAME = "NixOS";
+      };
+    };
+  };
+
+  testScript = ''
+    import shlex
+    import json
+
+    curl = "curl --fail-with-body --silent"
+    baseURL = "http://localhost:8080"
+    adminName = "admin"
+    adminEmail = "admin@nixos.org"
+    adminPass = "admin"
+    adminCreds = json.dumps({
+      "email": adminEmail,
+      "password": adminPass,
+    })
+    stopCode = 9 # SIGKILL
+    serverPort = 1337
+    serverDefinition = json.dumps({
+      "name": "netcat",
+      "node": 0,
+      "users": [
+        adminName,
+      ],
+      "type": "netcat",
+      "run": {
+        "stopCode": stopCode,
+        "command": f"nc -l {serverPort}",
+      },
+      "environment": {
+        "type": "standard",
+      },
+    })
+
+    start_all()
+
+    machine.wait_for_unit("pufferpanel.service")
+    machine.wait_for_open_port(5657) # SFTP
+    machine.wait_for_open_port(8080) # HTTP
+
+    # Note that PufferPanel does not initialize database unless necessary.
+    # /api/config endpoint creates database file and triggers migrations.
+    # On success, we run a command to create administrator user that we use to
+    # interact with HTTP API.
+    resp = json.loads(machine.succeed(f"{curl} {baseURL}/api/config"))
+    assert resp["branding"]["name"] == "NixOS", "Invalid company name in configuration"
+    assert resp["registrationEnabled"] == False, "Expected registration to be disabled"
+
+    machine.succeed(f"pufferpanel --workDir /var/lib/pufferpanel user add --admin --name {adminName} --email {adminEmail} --password {adminPass}")
+
+    resp = json.loads(machine.succeed(f"{curl} -d '{adminCreds}' {baseURL}/auth/login"))
+    assert "servers.admin" in resp["scopes"], "User is not administrator"
+    token = resp["session"]
+    authHeader = shlex.quote(f"Authorization: Bearer {token}")
+
+    resp = json.loads(machine.succeed(f"{curl} -H {authHeader} -H 'Content-Type: application/json' -d '{serverDefinition}' {baseURL}/api/servers"))
+    serverID = resp["id"]
+    machine.succeed(f"{curl} -X POST -H {authHeader} {baseURL}/proxy/daemon/server/{serverID}/start")
+    machine.wait_for_open_port(serverPort)
+  '';
+})
diff --git a/nixos/tests/qemu-vm-restrictnetwork.nix b/nixos/tests/qemu-vm-restrictnetwork.nix
new file mode 100644
index 00000000000..49a105ef107
--- /dev/null
+++ b/nixos/tests/qemu-vm-restrictnetwork.nix
@@ -0,0 +1,36 @@
+import ./make-test-python.nix ({
+  name = "qemu-vm-restrictnetwork";
+
+  nodes = {
+    unrestricted = { config, pkgs, ... }: {
+      virtualisation.restrictNetwork = false;
+    };
+
+    restricted = { config, pkgs, ... }: {
+      virtualisation.restrictNetwork = true;
+    };
+  };
+
+  testScript = ''
+    import os
+
+    if os.fork() == 0:
+      # Start some HTTP server on the qemu host to test guest isolation.
+      from http.server import HTTPServer, BaseHTTPRequestHandler
+      HTTPServer(("", 8000), BaseHTTPRequestHandler).serve_forever()
+
+    else:
+      start_all()
+      unrestricted.wait_for_unit("network-online.target")
+      restricted.wait_for_unit("network-online.target")
+
+      # Guests should be able to reach each other on the same VLAN.
+      unrestricted.succeed("ping -c1 restricted")
+      restricted.succeed("ping -c1 unrestricted")
+
+      # Only the unrestricted guest should be able to reach host services.
+      # 10.0.2.2 is the gateway mapping to the host's loopback interface.
+      unrestricted.succeed("curl -s http://10.0.2.2:8000")
+      restricted.fail("curl -s http://10.0.2.2:8000")
+    '';
+})
diff --git a/nixos/tests/quake3.nix b/nixos/tests/quake3.nix
index 82af1af463d..ef5fcc41476 100644
--- a/nixos/tests/quake3.nix
+++ b/nixos/tests/quake3.nix
@@ -1,4 +1,4 @@
-import ./make-test-python.nix ({ pkgs, ...} :
+import ./make-test-python.nix ({ pkgs, lib, ...} :
 
 let
 
@@ -11,9 +11,9 @@ let
     };
 
   # Only allow the demo data to be used (only if it's unfreeRedistributable).
-  unfreePredicate = pkg: with pkgs.lib; let
+  unfreePredicate = pkg: with lib; let
     allowPackageNames = [ "quake3-demodata" "quake3-pointrelease" ];
-    allowLicenses = [ pkgs.lib.licenses.unfreeRedistributable ];
+    allowLicenses = [ lib.licenses.unfreeRedistributable ];
   in elem pkg.pname allowPackageNames &&
      elem (pkg.meta.license or null) allowLicenses;
 
@@ -31,7 +31,7 @@ in
 
 rec {
   name = "quake3";
-  meta = with pkgs.stdenv.lib.maintainers; {
+  meta = with lib.maintainers; {
     maintainers = [ domenkozar eelco ];
   };
 
diff --git a/nixos/tests/readarr.nix b/nixos/tests/readarr.nix
new file mode 100644
index 00000000000..bb7dd852984
--- /dev/null
+++ b/nixos/tests/readarr.nix
@@ -0,0 +1,18 @@
+import ./make-test-python.nix ({ lib, ... }:
+
+with lib;
+
+{
+  name = "readarr";
+  meta.maintainers = with maintainers; [ jocelynthode ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    { services.readarr.enable = true; };
+
+  testScript = ''
+    machine.wait_for_unit("readarr.service")
+    machine.wait_for_open_port(8787)
+    machine.succeed("curl --fail http://localhost:8787/")
+  '';
+})
diff --git a/nixos/tests/restic.nix b/nixos/tests/restic.nix
index 3681c4cf190..626049e7334 100644
--- a/nixos/tests/restic.nix
+++ b/nixos/tests/restic.nix
@@ -2,22 +2,32 @@ import ./make-test-python.nix (
   { pkgs, ... }:
 
   let
-    remoteRepository = "/tmp/restic-backup";
-    remoteFromFileRepository = "/tmp/restic-backup-from-file";
-    rcloneRepository = "rclone:local:/tmp/restic-rclone-backup";
+    remoteRepository = "/root/restic-backup";
+    remoteFromFileRepository = "/root/restic-backup-from-file";
+    rcloneRepository = "rclone:local:/root/restic-rclone-backup";
 
     backupPrepareCommand = ''
-      touch /opt/backupPrepareCommand
-      test ! -e /opt/backupCleanupCommand
+      touch /root/backupPrepareCommand
+      test ! -e /root/backupCleanupCommand
     '';
 
     backupCleanupCommand = ''
-      rm /opt/backupPrepareCommand
-      touch /opt/backupCleanupCommand
+      rm /root/backupPrepareCommand
+      touch /root/backupCleanupCommand
     '';
 
+    testDir = pkgs.stdenvNoCC.mkDerivation {
+      name = "test-files-to-backup";
+      unpackPhase = "true";
+      installPhase = ''
+        mkdir $out
+        touch $out/some_file
+      '';
+    };
+
     passwordFile = "${pkgs.writeText "password" "correcthorsebatterystaple"}";
     paths = [ "/opt" ];
+    exclude = [ "/opt/excluded_file_*" ];
     pruneOpts = [
       "--keep-daily 2"
       "--keep-weekly 1"
@@ -38,17 +48,17 @@ import ./make-test-python.nix (
         {
           services.restic.backups = {
             remotebackup = {
-              inherit passwordFile paths pruneOpts backupPrepareCommand backupCleanupCommand;
+              inherit passwordFile paths exclude pruneOpts backupPrepareCommand backupCleanupCommand;
               repository = remoteRepository;
               initialize = true;
             };
             remote-from-file-backup = {
-              inherit passwordFile paths pruneOpts;
+              inherit passwordFile paths exclude pruneOpts;
               initialize = true;
               repositoryFile = pkgs.writeText "repositoryFile" remoteFromFileRepository;
             };
             rclonebackup = {
-              inherit passwordFile paths pruneOpts;
+              inherit passwordFile paths exclude pruneOpts;
               initialize = true;
               repository = rcloneRepository;
               rcloneConfig = {
@@ -71,7 +81,7 @@ import ./make-test-python.nix (
               inherit passwordFile paths;
               repository = "some-fake-repository";
               package = pkgs.writeShellScriptBin "restic" ''
-                echo "$@" >> /tmp/fake-restic.log;
+                echo "$@" >> /root/fake-restic.log;
               '';
 
               pruneOpts = [ "--keep-last 1" ];
@@ -90,20 +100,25 @@ import ./make-test-python.nix (
           "${pkgs.restic}/bin/restic -r ${remoteRepository} -p ${passwordFile} snapshots",
           '${pkgs.restic}/bin/restic -r ${remoteFromFileRepository} -p ${passwordFile} snapshots"',
           "${pkgs.restic}/bin/restic -r ${rcloneRepository} -p ${passwordFile} snapshots",
-          "grep 'backup .* /opt' /tmp/fake-restic.log",
+          "grep 'backup.* /opt' /root/fake-restic.log",
       )
       server.succeed(
           # set up
-          "mkdir -p /opt",
-          "touch /opt/some_file",
-          "mkdir -p /tmp/restic-rclone-backup",
+          "cp -rT ${testDir} /opt",
+          "touch /opt/excluded_file_1 /opt/excluded_file_2",
+          "mkdir -p /root/restic-rclone-backup",
 
           # test that remotebackup runs custom commands and produces a snapshot
           "timedatectl set-time '2016-12-13 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /root/backupCleanupCommand",
           '${pkgs.restic}/bin/restic -r ${remoteRepository} -p ${passwordFile} snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
 
+          # test that restoring that snapshot produces the same directory
+          "mkdir /tmp/restore-1",
+          "${pkgs.restic}/bin/restic -r ${remoteRepository} -p ${passwordFile} restore latest -t /tmp/restore-1",
+          "diff -ru ${testDir} /tmp/restore-1/opt",
+
           # test that remote-from-file-backup produces a snapshot
           "systemctl start restic-backups-remote-from-file-backup.service",
           '${pkgs.restic}/bin/restic -r ${remoteFromFileRepository} -p ${passwordFile} snapshots --json | ${pkgs.jq}/bin/jq "length | . == 1"',
@@ -114,33 +129,33 @@ import ./make-test-python.nix (
 
           # test that custompackage runs both `restic backup` and `restic check` with reasonable commandlines
           "systemctl start restic-backups-custompackage.service",
-          "grep 'backup .* /opt' /tmp/fake-restic.log",
-          "grep 'check .* --some-check-option' /tmp/fake-restic.log",
+          "grep 'backup.* /opt' /root/fake-restic.log",
+          "grep 'check.* --some-check-option' /root/fake-restic.log",
 
           # test that we can create four snapshots in remotebackup and rclonebackup
           "timedatectl set-time '2017-12-13 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /root/backupCleanupCommand",
           "systemctl start restic-backups-rclonebackup.service",
 
           "timedatectl set-time '2018-12-13 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /root/backupCleanupCommand",
           "systemctl start restic-backups-rclonebackup.service",
 
           "timedatectl set-time '2018-12-14 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /root/backupCleanupCommand",
           "systemctl start restic-backups-rclonebackup.service",
 
           "timedatectl set-time '2018-12-15 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /root/backupCleanupCommand",
           "systemctl start restic-backups-rclonebackup.service",
 
           "timedatectl set-time '2018-12-16 13:45'",
           "systemctl start restic-backups-remotebackup.service",
-          "rm /opt/backupCleanupCommand",
+          "rm /root/backupCleanupCommand",
           "systemctl start restic-backups-rclonebackup.service",
 
           '${pkgs.restic}/bin/restic -r ${remoteRepository} -p ${passwordFile} snapshots --json | ${pkgs.jq}/bin/jq "length | . == 4"',
diff --git a/nixos/tests/sgtpuzzles.nix b/nixos/tests/sgtpuzzles.nix
new file mode 100644
index 00000000000..b8d25d42d31
--- /dev/null
+++ b/nixos/tests/sgtpuzzles.nix
@@ -0,0 +1,34 @@
+import ./make-test-python.nix ({ pkgs, ...} :
+{
+  name = "sgtpuzzles";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ tomfitzhenry ];
+  };
+
+  nodes.machine = { ... }:
+
+  {
+    imports = [
+      ./common/x11.nix
+    ];
+
+    services.xserver.enable = true;
+    environment.systemPackages = with pkgs; [
+      sgtpuzzles
+    ];
+  };
+
+  enableOCR = true;
+
+  testScript = { nodes, ... }:
+  ''
+    start_all()
+    machine.wait_for_x()
+
+    machine.execute("mines >&2 &")
+
+    machine.wait_for_window("Mines")
+    machine.wait_for_text("Marked")
+    machine.screenshot("mines")
+  '';
+})
diff --git a/nixos/tests/shadow.nix b/nixos/tests/shadow.nix
index baa2e5945c0..c9a04088e87 100644
--- a/nixos/tests/shadow.nix
+++ b/nixos/tests/shadow.nix
@@ -5,6 +5,7 @@ let
   password4 = "asdf123";
   hashed_bcrypt = "$2b$05$8xIEflrk2RxQtcVXbGIxs.Vl0x7dF1/JSv3cyX6JJt0npzkTCWvxK"; # fnord
   hashed_yeshash = "$y$j9T$d8Z4EAf8P1SvM/aDFbxMS0$VnTXMp/Hnc7QdCBEaLTq5ZFOAFo2/PM0/xEAFuOE88."; # fnord
+  hashed_sha512crypt = "$6$ymzs8WINZ5wGwQcV$VC2S0cQiX8NVukOLymysTPn4v1zJoJp3NGyhnqyv/dAf4NWZsBWYveQcj6gEJr4ZUjRBRjM0Pj1L8TCQ8hUUp0"; # meow
 in import ./make-test-python.nix ({ pkgs, ... }: {
   name = "shadow";
   meta = with pkgs.lib.maintainers; { maintainers = [ nequissimus ]; };
@@ -39,6 +40,12 @@ in import ./make-test-python.nix ({ pkgs, ... }: {
         hashedPassword = hashed_yeshash;
         shell = pkgs.bash;
       };
+      users.leo = {
+        isNormalUser = true;
+        initialHashedPassword = "!";
+        hashedPassword = hashed_sha512crypt; # should take precedence over initialHashedPassword
+        shell = pkgs.bash;
+      };
     };
   };
 
@@ -145,5 +152,21 @@ in import ./make-test-python.nix ({ pkgs, ... }: {
             print(shadow.succeed(f"cat /tmp/{u}"))
             assert u in shadow.succeed(f"cat /tmp/{u}")
             shadow.send_chars("logout\n")
+
+    with subtest("Ensure hashedPassword does not get overridden by initialHashedPassword"):
+        shadow.send_key("alt-f6")
+        shadow.wait_until_succeeds("[ $(fgconsole) = 6 ]")
+        shadow.wait_for_unit("getty@tty6.service")
+        shadow.wait_until_succeeds("pgrep -f 'agetty.*tty6'")
+        shadow.wait_until_tty_matches("6", "login: ")
+        shadow.send_chars("leo\n")
+        shadow.wait_until_tty_matches("6", "login: leo")
+        shadow.wait_until_succeeds("pgrep login")
+        shadow.sleep(2)
+        shadow.send_chars("meow\n")
+        shadow.send_chars("whoami > /tmp/leo\n")
+        shadow.wait_for_file("/tmp/leo")
+        assert "leo" in shadow.succeed("cat /tmp/leo")
+        shadow.send_chars("logout\n")
   '';
 })
diff --git a/nixos/tests/soapui.nix b/nixos/tests/soapui.nix
index e4ce3888fd4..3a2d11a1675 100644
--- a/nixos/tests/soapui.nix
+++ b/nixos/tests/soapui.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "soapui";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ asbachb ];
+    maintainers = [ ];
   };
 
   nodes.machine = { config, pkgs, ... }: {
diff --git a/nixos/tests/solr.nix b/nixos/tests/solr.nix
deleted file mode 100644
index 33afe9d788f..00000000000
--- a/nixos/tests/solr.nix
+++ /dev/null
@@ -1,56 +0,0 @@
-import ./make-test-python.nix ({ pkgs, ... }:
-
-{
-  name = "solr";
-  meta.maintainers = [ pkgs.lib.maintainers.aanderse ];
-
-  nodes.machine =
-    { config, pkgs, ... }:
-    {
-      # Ensure the virtual machine has enough memory for Solr to avoid the following error:
-      #
-      #   OpenJDK 64-Bit Server VM warning:
-      #     INFO: os::commit_memory(0x00000000e8000000, 402653184, 0)
-      #     failed; error='Cannot allocate memory' (errno=12)
-      #
-      #   There is insufficient memory for the Java Runtime Environment to continue.
-      #   Native memory allocation (mmap) failed to map 402653184 bytes for committing reserved memory.
-      virtualisation.memorySize = 2000;
-
-      services.solr.enable = true;
-    };
-
-  testScript = ''
-    start_all()
-
-    machine.wait_for_unit("solr.service")
-    machine.wait_for_open_port(8983)
-    machine.succeed("curl --fail http://localhost:8983/solr/")
-
-    # adapted from pkgs.solr/examples/films/README.txt
-    machine.succeed("sudo -u solr solr create -c films")
-    assert '"status":0' in machine.succeed(
-        """
-      curl http://localhost:8983/solr/films/schema -X POST -H 'Content-type:application/json' --data-binary '{
-        "add-field" : {
-          "name":"name",
-          "type":"text_general",
-          "multiValued":false,
-          "stored":true
-        },
-        "add-field" : {
-          "name":"initial_release_date",
-          "type":"pdate",
-          "stored":true
-        }
-      }'
-    """
-    )
-    machine.succeed(
-        "sudo -u solr post -c films ${pkgs.solr}/example/films/films.json"
-    )
-    assert '"name":"Batman Begins"' in machine.succeed(
-        "curl http://localhost:8983/solr/films/query?q=name:batman"
-    )
-  '';
-})
diff --git a/nixos/tests/sourcehut.nix b/nixos/tests/sourcehut.nix
index 9caa1bcd98f..87e6d82bdd8 100644
--- a/nixos/tests/sourcehut.nix
+++ b/nixos/tests/sourcehut.nix
@@ -18,8 +18,10 @@ let
           # passwordless ssh server
           services.openssh = {
             enable = true;
-            permitRootLogin = "yes";
-            extraConfig = "PermitEmptyPasswords yes";
+            settings = {
+              PermitRootLogin = "yes";
+              PermitEmptyPasswords = true;
+            };
           };
 
           users = {
diff --git a/nixos/tests/specialisation.nix b/nixos/tests/specialisation.nix
deleted file mode 100644
index b8d4b8279f4..00000000000
--- a/nixos/tests/specialisation.nix
+++ /dev/null
@@ -1,43 +0,0 @@
-import ./make-test-python.nix {
-  name = "specialisation";
-  nodes =  {
-    inheritconf = { pkgs, ... }: {
-      environment.systemPackages = [ pkgs.cowsay ];
-      specialisation.inheritconf.configuration = { pkgs, ... }: {
-        environment.systemPackages = [ pkgs.hello ];
-      };
-    };
-    noinheritconf = { pkgs, ... }: {
-      environment.systemPackages = [ pkgs.cowsay ];
-      specialisation.noinheritconf = {
-        inheritParentConfig = false;
-        configuration = { pkgs, ... }: {
-          environment.systemPackages = [ pkgs.hello ];
-        };
-      };
-    };
-  };
-  testScript = ''
-    inheritconf.wait_for_unit("default.target")
-    inheritconf.succeed("cowsay hey")
-    inheritconf.fail("hello")
-
-    with subtest("Nested clones do inherit from parent"):
-        inheritconf.succeed(
-            "/run/current-system/specialisation/inheritconf/bin/switch-to-configuration test"
-        )
-        inheritconf.succeed("cowsay hey")
-        inheritconf.succeed("hello")
-
-        noinheritconf.wait_for_unit("default.target")
-        noinheritconf.succeed("cowsay hey")
-        noinheritconf.fail("hello")
-
-    with subtest("Nested children do not inherit from parent"):
-        noinheritconf.succeed(
-            "/run/current-system/specialisation/noinheritconf/bin/switch-to-configuration test"
-        )
-        noinheritconf.fail("cowsay hey")
-        noinheritconf.succeed("hello")
-  '';
-}
diff --git a/nixos/tests/sssd.nix b/nixos/tests/sssd.nix
index 25527cb59a5..c8d356e074a 100644
--- a/nixos/tests/sssd.nix
+++ b/nixos/tests/sssd.nix
@@ -13,5 +13,6 @@ import ./make-test-python.nix ({ pkgs, ... }:
       start_all()
       machine.wait_for_unit("multi-user.target")
       machine.wait_for_unit("sssd.service")
+      machine.succeed("sssctl config-check")
     '';
 })
diff --git a/nixos/tests/stratis/simple.nix b/nixos/tests/stratis/simple.nix
index 7357d71fc52..543789f59c0 100644
--- a/nixos/tests/stratis/simple.nix
+++ b/nixos/tests/stratis/simple.nix
@@ -8,7 +8,7 @@ import ../make-test-python.nix ({ pkgs, ... }:
 
     nodes.machine = { pkgs, ... }: {
       services.stratis.enable = true;
-      virtualisation.emptyDiskImages = [ 1024 1024 1024 1024 ];
+      virtualisation.emptyDiskImages = [ 2048 1024 1024 1024 ];
     };
 
     testScript = ''
diff --git a/nixos/tests/swap-file-btrfs.nix b/nixos/tests/swap-file-btrfs.nix
new file mode 100644
index 00000000000..d9fcd2be116
--- /dev/null
+++ b/nixos/tests/swap-file-btrfs.nix
@@ -0,0 +1,46 @@
+import ./make-test-python.nix ({ lib, ... }:
+{
+  name = "swap-file-btrfs";
+
+  meta.maintainers = with lib.maintainers; [ oxalica ];
+
+  nodes.machine =
+    { pkgs, ... }:
+    {
+      virtualisation.useDefaultFilesystems = false;
+
+      virtualisation.rootDevice = "/dev/vda";
+
+      boot.initrd.postDeviceCommands = ''
+        ${pkgs.btrfs-progs}/bin/mkfs.btrfs --label root /dev/vda
+      '';
+
+      virtualisation.fileSystems = {
+        "/" = {
+          device = "/dev/disk/by-label/root";
+          fsType = "btrfs";
+        };
+      };
+
+      swapDevices = [
+        {
+          device = "/var/swapfile";
+          size = 1; # 1MiB.
+        }
+      ];
+    };
+
+  testScript = ''
+    machine.wait_for_unit('var-swapfile.swap')
+    machine.succeed("stat --file-system --format=%T /var/swapfile | grep btrfs")
+    # First run. Auto creation.
+    machine.succeed("swapon --show | grep /var/swapfile")
+
+    machine.shutdown()
+    machine.start()
+
+    # Second run. Use it as-is.
+    machine.wait_for_unit('var-swapfile.swap')
+    machine.succeed("swapon --show | grep /var/swapfile")
+  '';
+})
diff --git a/nixos/tests/swap-partition.nix b/nixos/tests/swap-partition.nix
index 2279630b57b..ddcaeb95453 100644
--- a/nixos/tests/swap-partition.nix
+++ b/nixos/tests/swap-partition.nix
@@ -7,7 +7,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }:
     {
       virtualisation.useDefaultFilesystems = false;
 
-      virtualisation.bootDevice = "/dev/vda1";
+      virtualisation.rootDevice = "/dev/vda1";
 
       boot.initrd.postDeviceCommands = ''
         if ! test -b /dev/vda1; then
diff --git a/nixos/tests/systemd-boot.nix b/nixos/tests/systemd-boot.nix
index 039e6bdd9d5..94e269ff37b 100644
--- a/nixos/tests/systemd-boot.nix
+++ b/nixos/tests/systemd-boot.nix
@@ -101,13 +101,13 @@ in
       # Replace version inside sd-boot with something older. See magic[] string in systemd src/boot/efi/boot.c
       machine.succeed(
           """
-        find /boot -iname '*.efi' -print0 | \
+        find /boot -iname '*boot*.efi' -print0 | \
         xargs -0 -I '{}' sed -i 's/#### LoaderInfo: systemd-boot .* ####/#### LoaderInfo: systemd-boot 000.0-1-notnixos ####/' '{}'
       """
       )
 
       output = machine.succeed("/run/current-system/bin/switch-to-configuration boot")
-      assert "updating systemd-boot from (000.0-1-notnixos) to " in output
+      assert "updating systemd-boot from 000.0-1-notnixos to " in output
     '';
   };
 
diff --git a/nixos/tests/systemd-credentials-tpm2.nix b/nixos/tests/systemd-credentials-tpm2.nix
new file mode 100644
index 00000000000..d2dc1fd7b61
--- /dev/null
+++ b/nixos/tests/systemd-credentials-tpm2.nix
@@ -0,0 +1,124 @@
+import ./make-test-python.nix ({ lib, pkgs, system, ... }:
+
+let
+  tpmSocketPath = "/tmp/swtpm-sock";
+  tpmDeviceModels = {
+    x86_64-linux = "tpm-tis";
+    aarch64-linux = "tpm-tis-device";
+  };
+in
+
+{
+  name = "systemd-credentials-tpm2";
+
+  meta = {
+    maintainers = with pkgs.lib.maintainers; [ tmarkus ];
+  };
+
+  nodes.machine = { pkgs, ... }: {
+    virtualisation = {
+      qemu.options = [
+        "-chardev socket,id=chrtpm,path=${tpmSocketPath}"
+        "-tpmdev emulator,id=tpm_dev_0,chardev=chrtpm"
+        "-device ${tpmDeviceModels.${system}},tpmdev=tpm_dev_0"
+      ];
+    };
+
+    boot.initrd.availableKernelModules = [ "tpm_tis" ];
+
+    environment.systemPackages = with pkgs; [ diffutils ];
+  };
+
+  testScript = ''
+    import subprocess
+    from tempfile import TemporaryDirectory
+
+    # From systemd-initrd-luks-tpm2.nix
+    class Tpm:
+        def __init__(self):
+            self.state_dir = TemporaryDirectory()
+            self.start()
+
+        def start(self):
+            self.proc = subprocess.Popen(["${pkgs.swtpm}/bin/swtpm",
+                "socket",
+                "--tpmstate", f"dir={self.state_dir.name}",
+                "--ctrl", "type=unixio,path=${tpmSocketPath}",
+                "--tpm2",
+                ])
+
+            # Check whether starting swtpm failed
+            try:
+                exit_code = self.proc.wait(timeout=0.2)
+                if exit_code is not None and exit_code != 0:
+                    raise Exception("failed to start swtpm")
+            except subprocess.TimeoutExpired:
+                pass
+
+        """Check whether the swtpm process exited due to an error"""
+        def check(self):
+            exit_code = self.proc.poll()
+            if exit_code is not None and exit_code != 0:
+                raise Exception("swtpm process died")
+
+    CRED_NAME = "testkey"
+    CRED_RAW_FILE = f"/root/{CRED_NAME}"
+    CRED_FILE = f"/root/{CRED_NAME}.cred"
+
+    def systemd_run(machine, cmd):
+        machine.log(f"Executing command (via systemd-run): \"{cmd}\"")
+
+        (status, out) = machine.execute( " ".join([
+            "systemd-run",
+            "--service-type=exec",
+            "--quiet",
+            "--wait",
+            "-E PATH=\"$PATH\"",
+            "-p StandardOutput=journal",
+            "-p StandardError=journal",
+            f"-p LoadCredentialEncrypted={CRED_NAME}:{CRED_FILE}",
+            f"$SHELL -c '{cmd}'"
+            ]) )
+
+        if status != 0:
+            raise Exception(f"systemd_run failed (status {status})")
+
+        machine.log("systemd-run finished successfully")
+
+    tpm = Tpm()
+
+    @polling_condition
+    def swtpm_running():
+        tpm.check()
+
+    machine.wait_for_unit("multi-user.target")
+
+    with subtest("Check whether TPM device exists"):
+        machine.succeed("test -e /dev/tpm0")
+        machine.succeed("test -e /dev/tpmrm0")
+
+    with subtest("Check whether systemd-creds detects TPM2 correctly"):
+        cmd = "systemd-creds has-tpm2"
+        machine.log(f"Running \"{cmd}\"")
+        (status, _) = machine.execute(cmd)
+
+        # Check exit code equals 0 or 1 (1 means firmware support is missing, which is OK here)
+        if status != 0 and status != 1:
+            raise Exception("systemd-creds failed to detect TPM2")
+
+    with subtest("Encrypt credential using systemd-creds"):
+        machine.succeed(f"dd if=/dev/urandom of={CRED_RAW_FILE} bs=1k count=16")
+        machine.succeed(f"systemd-creds --with-key=host+tpm2 encrypt --name=testkey {CRED_RAW_FILE} {CRED_FILE}")
+
+    with subtest("Write provided credential and check for equality"):
+        CRED_OUT_FILE = f"/root/{CRED_NAME}.out"
+        systemd_run(machine, f"systemd-creds cat testkey > {CRED_OUT_FILE}")
+        machine.succeed(f"cmp --silent -- {CRED_RAW_FILE} {CRED_OUT_FILE}")
+
+    with subtest("Check whether systemd service can see credential in systemd-creds list"):
+        systemd_run(machine, f"systemd-creds list | grep {CRED_NAME}")
+
+    with subtest("Check whether systemd service can access credential in $CREDENTIALS_DIRECTORY"):
+        systemd_run(machine, f"cmp --silent -- $CREDENTIALS_DIRECTORY/{CRED_NAME} {CRED_RAW_FILE}")
+  '';
+})
diff --git a/nixos/tests/systemd-cryptenroll.nix b/nixos/tests/systemd-cryptenroll.nix
index 9ee2d280fbb..055ae7d1681 100644
--- a/nixos/tests/systemd-cryptenroll.nix
+++ b/nixos/tests/systemd-cryptenroll.nix
@@ -2,7 +2,6 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   name = "systemd-cryptenroll";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ ymatsiuk ];
-    broken = true; # times out after two hours, details -> https://github.com/NixOS/nixpkgs/issues/167994
   };
 
   nodes.machine = { pkgs, lib, ... }: {
diff --git a/nixos/tests/systemd-homed.nix b/nixos/tests/systemd-homed.nix
new file mode 100644
index 00000000000..ecc92e98edd
--- /dev/null
+++ b/nixos/tests/systemd-homed.nix
@@ -0,0 +1,99 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }:
+let
+  password = "foobar";
+  newPass = "barfoo";
+in
+{
+  name = "systemd-homed";
+  nodes.machine = { config, pkgs, ... }: {
+    services.homed.enable = true;
+
+    users.users.test-normal-user = {
+      extraGroups = [ "wheel" ];
+      isNormalUser = true;
+      initialPassword = password;
+    };
+  };
+  testScript = ''
+    def switchTTY(number):
+      machine.send_key(f"alt-f{number}")
+      machine.wait_until_succeeds(f"[ $(fgconsole) = {number} ]")
+      machine.wait_for_unit(f"getty@tty{number}.service")
+      machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{number}'")
+
+    machine.wait_for_unit("multi-user.target")
+
+    # Smoke test to make sure the pam changes didn't break regular users.
+    machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
+    with subtest("login as regular user"):
+      switchTTY(2)
+      machine.wait_until_tty_matches("2", "login: ")
+      machine.send_chars("test-normal-user\n")
+      machine.wait_until_tty_matches("2", "login: test-normal-user")
+      machine.wait_until_tty_matches("2", "Password: ")
+      machine.send_chars("${password}\n")
+      machine.wait_until_succeeds("pgrep -u test-normal-user bash")
+      machine.send_chars("whoami > /tmp/1\n")
+      machine.wait_for_file("/tmp/1")
+      assert "test-normal-user" in machine.succeed("cat /tmp/1")
+
+    with subtest("create homed encrypted user"):
+      # TODO: Figure out how to pass password manually.
+      #
+      # This environment variable is used for homed internal testing
+      # and is not documented.
+      machine.succeed("NEWPASSWORD=${password} homectl create --shell=/run/current-system/sw/bin/bash --storage=luks -G wheel test-homed-user")
+
+    with subtest("login as homed user"):
+      switchTTY(3)
+      machine.wait_until_tty_matches("3", "login: ")
+      machine.send_chars("test-homed-user\n")
+      machine.wait_until_tty_matches("3", "login: test-homed-user")
+      machine.wait_until_tty_matches("3", "Password: ")
+      machine.send_chars("${password}\n")
+      machine.wait_until_succeeds("pgrep -t tty3 -u test-homed-user bash")
+      machine.send_chars("whoami > /tmp/2\n")
+      machine.wait_for_file("/tmp/2")
+      assert "test-homed-user" in machine.succeed("cat /tmp/2")
+
+    with subtest("change homed user password"):
+      switchTTY(4)
+      machine.wait_until_tty_matches("4", "login: ")
+      machine.send_chars("test-homed-user\n")
+      machine.wait_until_tty_matches("4", "login: test-homed-user")
+      machine.wait_until_tty_matches("4", "Password: ")
+      machine.send_chars("${password}\n")
+      machine.wait_until_succeeds("pgrep -t tty4 -u test-homed-user bash")
+      machine.send_chars("passwd\n")
+      # homed does it in a weird order, it asks for new passes, then it asks
+      # for the old one.
+      machine.sleep(2)
+      machine.send_chars("${newPass}\n")
+      machine.sleep(2)
+      machine.send_chars("${newPass}\n")
+      machine.sleep(4)
+      machine.send_chars("${password}\n")
+      machine.wait_until_fails("pgrep -t tty4 passwd")
+
+      @polling_condition
+      def not_logged_in_tty5():
+        machine.fail("pgrep -t tty5 bash")
+
+      switchTTY(5)
+      with not_logged_in_tty5: # type: ignore[union-attr]
+        machine.wait_until_tty_matches("5", "login: ")
+        machine.send_chars("test-homed-user\n")
+        machine.wait_until_tty_matches("5", "login: test-homed-user")
+        machine.wait_until_tty_matches("5", "Password: ")
+        machine.send_chars("${password}\n")
+        machine.wait_until_tty_matches("5", "Password incorrect or not sufficient for authentication of user test-homed-user.")
+        machine.wait_until_tty_matches("5", "Sorry, try again: ")
+      machine.send_chars("${newPass}\n")
+      machine.send_chars("whoami > /tmp/4\n")
+      machine.wait_for_file("/tmp/4")
+      assert "test-homed-user" in machine.succeed("cat /tmp/4")
+
+    with subtest("homed user should be in wheel according to NSS"):
+      machine.succeed("userdbctl group wheel -s io.systemd.NameServiceSwitch | grep test-homed-user")
+  '';
+})
diff --git a/nixos/tests/systemd-initrd-btrfs-raid.nix b/nixos/tests/systemd-initrd-btrfs-raid.nix
index 40fd2d4dc61..c9cdf0060b1 100644
--- a/nixos/tests/systemd-initrd-btrfs-raid.nix
+++ b/nixos/tests/systemd-initrd-btrfs-raid.nix
@@ -21,14 +21,14 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
       fileSystems = lib.mkVMOverride {
         "/".fsType = lib.mkForce "btrfs";
       };
-      virtualisation.bootDevice = "/dev/vdc";
+      virtualisation.rootDevice = "/dev/vdb";
     };
   };
 
   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")
+    machine.succeed("mkfs.btrfs -d raid0 /dev/vdb /dev/vdc")
+    machine.succeed("mkdir -p /mnt && mount /dev/vdb /mnt && echo hello > /mnt/test && umount /mnt")
 
     # Boot from the RAID
     machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-btrfs-raid.conf")
@@ -38,7 +38,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
 
     # 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 "/dev/vdb 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-fido2.nix b/nixos/tests/systemd-initrd-luks-fido2.nix
index 133e552a3dc..e80d95f79c7 100644
--- a/nixos/tests/systemd-initrd-luks-fido2.nix
+++ b/nixos/tests/systemd-initrd-luks-fido2.nix
@@ -19,19 +19,19 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
     specialisation.boot-luks.configuration = {
       boot.initrd.luks.devices = lib.mkVMOverride {
         cryptroot = {
-          device = "/dev/vdc";
+          device = "/dev/vdb";
           crypttabExtraOpts = [ "fido2-device=auto" ];
         };
       };
-      virtualisation.bootDevice = "/dev/mapper/cryptroot";
+      virtualisation.rootDevice = "/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("PASSWORD=supersecret SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll --fido2-device=auto /dev/vdc |& systemd-cat")
+    machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdb -")
+    machine.succeed("PASSWORD=supersecret SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll --fido2-device=auto /dev/vdb |& systemd-cat")
 
     # Boot from the encrypted disk
     machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf")
diff --git a/nixos/tests/systemd-initrd-luks-keyfile.nix b/nixos/tests/systemd-initrd-luks-keyfile.nix
index 25c0c5bd866..257243d92a1 100644
--- a/nixos/tests/systemd-initrd-luks-keyfile.nix
+++ b/nixos/tests/systemd-initrd-luks-keyfile.nix
@@ -27,11 +27,11 @@ in {
     specialisation.boot-luks.configuration = {
       boot.initrd.luks.devices = lib.mkVMOverride {
         cryptroot = {
-          device = "/dev/vdc";
+          device = "/dev/vdb";
           keyFile = "/etc/cryptroot.key";
         };
       };
-      virtualisation.bootDevice = "/dev/mapper/cryptroot";
+      virtualisation.rootDevice = "/dev/mapper/cryptroot";
       boot.initrd.secrets."/etc/cryptroot.key" = keyfile;
     };
   };
@@ -39,7 +39,7 @@ in {
   testScript = ''
     # Create encrypted volume
     machine.wait_for_unit("multi-user.target")
-    machine.succeed("cryptsetup luksFormat -q --iter-time=1 -d ${keyfile} /dev/vdc")
+    machine.succeed("cryptsetup luksFormat -q --iter-time=1 -d ${keyfile} /dev/vdb")
 
     # Boot from the encrypted disk
     machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf")
diff --git a/nixos/tests/systemd-initrd-luks-password.nix b/nixos/tests/systemd-initrd-luks-password.nix
index 55d0b4324b4..2dd3f304e82 100644
--- a/nixos/tests/systemd-initrd-luks-password.nix
+++ b/nixos/tests/systemd-initrd-luks-password.nix
@@ -19,10 +19,10 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
     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";
+        cryptroot.device = "/dev/vdb";
+        cryptroot2.device = "/dev/vdc";
       };
-      virtualisation.bootDevice = "/dev/mapper/cryptroot";
+      virtualisation.rootDevice = "/dev/mapper/cryptroot";
       # test mounting device unlocked in initrd after switching root
       virtualisation.fileSystems."/cryptroot2".device = "/dev/mapper/cryptroot2";
     };
@@ -31,9 +31,9 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
   testScript = ''
     # Create encrypted volume
     machine.wait_for_unit("multi-user.target")
+    machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdb -")
     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 -")
-    machine.succeed("echo -n supersecret | cryptsetup luksOpen   -q               /dev/vdd cryptroot2")
+    machine.succeed("echo -n supersecret | cryptsetup luksOpen   -q               /dev/vdc cryptroot2")
     machine.succeed("mkfs.ext4 /dev/mapper/cryptroot2")
 
     # Boot from the encrypted disk
@@ -47,7 +47,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
     machine.send_console("supersecret\n")
     machine.wait_for_unit("multi-user.target")
 
-    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount")
+    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount"), "/dev/mapper/cryptroot do not appear in mountpoints list"
     assert "/dev/mapper/cryptroot2 on /cryptroot2 type ext4" in machine.succeed("mount")
   '';
 })
diff --git a/nixos/tests/systemd-initrd-luks-tpm2.nix b/nixos/tests/systemd-initrd-luks-tpm2.nix
index 085088d2ee2..734ef38579f 100644
--- a/nixos/tests/systemd-initrd-luks-tpm2.nix
+++ b/nixos/tests/systemd-initrd-luks-tpm2.nix
@@ -21,11 +21,11 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
     specialisation.boot-luks.configuration = {
       boot.initrd.luks.devices = lib.mkVMOverride {
         cryptroot = {
-          device = "/dev/vdc";
+          device = "/dev/vdb";
           crypttabExtraOpts = [ "tpm2-device=auto" ];
         };
       };
-      virtualisation.bootDevice = "/dev/mapper/cryptroot";
+      virtualisation.rootDevice = "/dev/mapper/cryptroot";
     };
   };
 
@@ -55,8 +55,8 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
 
     # 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("PASSWORD=supersecret SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll --tpm2-pcrs= --tpm2-device=auto /dev/vdc |& systemd-cat")
+    machine.succeed("echo -n supersecret | cryptsetup luksFormat -q --iter-time=1 /dev/vdb -")
+    machine.succeed("PASSWORD=supersecret SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll --tpm2-pcrs= --tpm2-device=auto /dev/vdb |& systemd-cat")
 
     # Boot from the encrypted disk
     machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf")
diff --git a/nixos/tests/systemd-initrd-networkd-ssh.nix b/nixos/tests/systemd-initrd-networkd-ssh.nix
new file mode 100644
index 00000000000..943552613be
--- /dev/null
+++ b/nixos/tests/systemd-initrd-networkd-ssh.nix
@@ -0,0 +1,82 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "systemd-initrd-network-ssh";
+  meta.maintainers = [ lib.maintainers.elvishjerricco ];
+
+  nodes = with lib; {
+    server = { config, pkgs, ... }: {
+      environment.systemPackages = [pkgs.cryptsetup];
+      boot.loader.systemd-boot.enable = true;
+      boot.loader.timeout = 0;
+      virtualisation = {
+        emptyDiskImages = [ 4096 ];
+        useBootLoader = true;
+        useEFIBoot = true;
+      };
+
+      specialisation.encrypted-root.configuration = {
+        virtualisation.bootDevice = "/dev/mapper/root";
+        boot.initrd.luks.devices = lib.mkVMOverride {
+          root.device = "/dev/vdc";
+        };
+        boot.initrd.systemd.enable = true;
+        boot.initrd.network = {
+          enable = true;
+          ssh = {
+            enable = true;
+            authorizedKeys = [ (readFile ./initrd-network-ssh/id_ed25519.pub) ];
+            port = 22;
+            # Terrible hack so it works with useBootLoader
+            hostKeys = [ { outPath = "${./initrd-network-ssh/ssh_host_ed25519_key}"; } ];
+          };
+        };
+      };
+    };
+
+    client = { config, ... }: {
+      environment.etc = {
+        knownHosts = {
+          text = concatStrings [
+            "server,"
+            "${
+              toString (head (splitString " " (toString
+                (elemAt (splitString "\n" config.networking.extraHosts) 2))))
+            } "
+            "${readFile ./initrd-network-ssh/ssh_host_ed25519_key.pub}"
+          ];
+        };
+        sshKey = {
+          source = ./initrd-network-ssh/id_ed25519;
+          mode = "0600";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+
+    def ssh_is_up(_) -> bool:
+        status, _ = client.execute("nc -z server 22")
+        return status == 0
+
+    server.wait_for_unit("multi-user.target")
+    server.succeed(
+        "echo somepass | cryptsetup luksFormat --type=luks2 /dev/vdc",
+        "bootctl set-default nixos-generation-1-specialisation-encrypted-root.conf",
+        "sync",
+    )
+    server.shutdown()
+    server.start()
+
+    client.wait_for_unit("network.target")
+    with client.nested("waiting for SSH server to come up"):
+        retry(ssh_is_up)
+
+    client.succeed(
+        "echo somepass | ssh -i /etc/sshKey -o UserKnownHostsFile=/etc/knownHosts server 'systemd-tty-ask-password-agent' & exit"
+    )
+
+    server.wait_for_unit("multi-user.target")
+    server.succeed("mount | grep '/dev/mapper/root on /'")
+  '';
+})
diff --git a/nixos/tests/systemd-initrd-networkd.nix b/nixos/tests/systemd-initrd-networkd.nix
new file mode 100644
index 00000000000..00ecbec5613
--- /dev/null
+++ b/nixos/tests/systemd-initrd-networkd.nix
@@ -0,0 +1,74 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "systemd-initrd-network";
+  meta.maintainers = [ lib.maintainers.elvishjerricco ];
+
+  nodes = let
+    mkFlushTest = flush: script: { ... }: {
+      boot.initrd.systemd.enable = true;
+      boot.initrd.network = {
+        enable = true;
+        flushBeforeStage2 = flush;
+      };
+      systemd.services.check-flush = {
+        requiredBy = ["multi-user.target"];
+        before = ["network-pre.target" "multi-user.target"];
+        unitConfig.DefaultDependencies = false;
+        serviceConfig.Type = "oneshot";
+        path = [ pkgs.iproute2 pkgs.iputils pkgs.gnugrep ];
+        inherit script;
+      };
+    };
+  in {
+    basic = { ... }: {
+      boot.initrd.network.enable = true;
+
+      boot.initrd.systemd = {
+        enable = true;
+        # Enable network-online to fail the test in case of timeout
+        network.wait-online.timeout = 10;
+        network.wait-online.anyInterface = true;
+        targets.network-online.requiredBy = [ "initrd.target" ];
+        services.systemd-networkd-wait-online.requiredBy =
+          [ "network-online.target" ];
+
+          initrdBin = [ pkgs.iproute2 pkgs.iputils pkgs.gnugrep ];
+          services.check = {
+            requiredBy = [ "initrd.target" ];
+            before = [ "initrd.target" ];
+            after = [ "network-online.target" ];
+            serviceConfig.Type = "oneshot";
+            path = [ pkgs.iproute2 pkgs.iputils pkgs.gnugrep ];
+            script = ''
+              ip addr | grep 10.0.2.15 || exit 1
+              ping -c1 10.0.2.2 || exit 1
+            '';
+          };
+      };
+    };
+
+    doFlush = mkFlushTest true ''
+      if ip addr | grep 10.0.2.15; then
+        echo "Network configuration survived switch-root; flushBeforeStage2 failed"
+        exit 1
+      fi
+    '';
+
+    dontFlush = mkFlushTest false ''
+      if ! (ip addr | grep 10.0.2.15); then
+        echo "Network configuration didn't survive switch-root"
+        exit 1
+      fi
+    '';
+  };
+
+  testScript = ''
+    start_all()
+    basic.wait_for_unit("multi-user.target")
+    doFlush.wait_for_unit("multi-user.target")
+    dontFlush.wait_for_unit("multi-user.target")
+    # Make sure the systemd-network user was set correctly in initrd
+    basic.succeed("[ $(stat -c '%U,%G' /run/systemd/netif/links) = systemd-network,systemd-network ]")
+    basic.succeed("ip addr show >&2")
+    basic.succeed("ip route show >&2")
+  '';
+})
diff --git a/nixos/tests/systemd-initrd-simple.nix b/nixos/tests/systemd-initrd-simple.nix
index 5d98114304b..a6a22e9d48e 100644
--- a/nixos/tests/systemd-initrd-simple.nix
+++ b/nixos/tests/systemd-initrd-simple.nix
@@ -6,9 +6,7 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
       enable = true;
       emergencyAccess = true;
     };
-    fileSystems = lib.mkVMOverride {
-      "/".autoResize = true;
-    };
+    virtualisation.fileSystems."/".autoResize = true;
   };
 
   testScript = ''
@@ -29,6 +27,8 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
         machine.succeed("[ -e /dev/pts/ptmx ]") # /dev/pts
         machine.succeed("[ -e /run/keys ]") # /run/keys
 
+    with subtest("groups work"):
+        machine.fail("journalctl -b 0 | grep 'systemd-udevd.*Unknown group.*ignoring'")
 
     with subtest("growfs works"):
         oldAvail = machine.succeed("df --output=avail / | sed 1d")
diff --git a/nixos/tests/systemd-initrd-swraid.nix b/nixos/tests/systemd-initrd-swraid.nix
index 28a0fb3192a..d201ba99a20 100644
--- a/nixos/tests/systemd-initrd-swraid.nix
+++ b/nixos/tests/systemd-initrd-swraid.nix
@@ -20,18 +20,18 @@ import ./make-test-python.nix ({ lib, pkgs, ... }: {
       services.swraid = {
         enable = true;
         mdadmConf = ''
-          ARRAY /dev/md0 devices=/dev/vdc,/dev/vdd
+          ARRAY /dev/md0 devices=/dev/vdb,/dev/vdc
         '';
       };
       kernelModules = [ "raid0" ];
     };
 
-    specialisation.boot-swraid.configuration.virtualisation.bootDevice = "/dev/disk/by-label/testraid";
+    specialisation.boot-swraid.configuration.virtualisation.rootDevice = "/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("mdadm --create --force /dev/md0 -n 2 --level=raid0 /dev/vdb /dev/vdc")
     machine.succeed("mkfs.ext4 -L testraid /dev/md0")
     machine.succeed("mkdir -p /mnt && mount /dev/md0 /mnt && echo hello > /mnt/test && umount /mnt")
 
diff --git a/nixos/tests/systemd-initrd-vconsole.nix b/nixos/tests/systemd-initrd-vconsole.nix
new file mode 100644
index 00000000000..b74df410c42
--- /dev/null
+++ b/nixos/tests/systemd-initrd-vconsole.nix
@@ -0,0 +1,33 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: {
+  name = "systemd-initrd-vconsole";
+
+  nodes.machine = { pkgs, ... }: {
+    boot.kernelParams = [ "rd.systemd.unit=rescue.target" ];
+
+    boot.initrd.systemd = {
+      enable = true;
+      emergencyAccess = true;
+    };
+
+    console = {
+      earlySetup = true;
+      keyMap = "colemak";
+    };
+  };
+
+  testScript = ''
+    # Boot into rescue shell in initrd
+    machine.start()
+    machine.wait_for_console_text("Press Enter for maintenance")
+    machine.send_console("\n")
+    machine.wait_for_console_text("Logging in with home")
+
+    # Check keymap
+    machine.send_console("(printf '%s to receive text: \\n' Ready && read text && echo \"$text\") </dev/tty1\n")
+    machine.wait_for_console_text("Ready to receive text:")
+    for key in "asdfjkl;\n":
+      machine.send_key(key)
+    machine.wait_for_console_text("arstneio")
+    machine.send_console("systemctl poweroff\n")
+  '';
+})
diff --git a/nixos/tests/systemd-repart.nix b/nixos/tests/systemd-repart.nix
new file mode 100644
index 00000000000..36de5d988fd
--- /dev/null
+++ b/nixos/tests/systemd-repart.nix
@@ -0,0 +1,134 @@
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  # A testScript fragment that prepares a disk with some empty, unpartitioned
+  # space. and uses it to boot the test with. Takes a single argument `machine`
+  # from which the diskImage is extraced.
+  useDiskImage = machine: ''
+    import os
+    import shutil
+    import subprocess
+    import tempfile
+
+    tmp_disk_image = tempfile.NamedTemporaryFile()
+
+    shutil.copyfile("${machine.system.build.diskImage}/nixos.img", tmp_disk_image.name)
+
+    subprocess.run([
+      "${pkgs.qemu}/bin/qemu-img",
+      "resize",
+      "-f",
+      "raw",
+      tmp_disk_image.name,
+      "+32M",
+    ])
+
+    # Fix the GPT table by moving the backup table to the end of the enlarged
+    # disk image. This is necessary because we increased the size of the disk
+    # before. The disk needs to be a raw disk because sgdisk can only run on
+    # raw images.
+    subprocess.run([
+      "${pkgs.gptfdisk}/bin/sgdisk",
+      "--move-second-header",
+      tmp_disk_image.name,
+    ])
+
+    # Set NIX_DISK_IMAGE so that the qemu script finds the right disk image.
+    os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name
+  '';
+
+  common = { config, pkgs, lib, ... }: {
+    virtualisation.useDefaultFilesystems = false;
+    virtualisation.fileSystems = {
+      "/" = {
+        device = "/dev/vda2";
+        fsType = "ext4";
+      };
+    };
+
+    # systemd-repart operates on disks with a partition table. The qemu module,
+    # however, creates separate filesystem images without a partition table, so
+    # we have to create a disk image manually.
+    #
+    # This creates two partitions, an ESP mounted on /dev/vda1 and the root
+    # partition mounted on /dev/vda2
+    system.build.diskImage = import ../lib/make-disk-image.nix {
+      inherit config pkgs lib;
+      # Use a raw format disk so that it can be resized before starting the
+      # test VM.
+      format = "raw";
+      # Keep the image as small as possible but leave some room for changes.
+      bootSize = "32M";
+      additionalSpace = "0M";
+      # GPT with an EFI System Partition is the typical use case for
+      # systemd-repart because it does not support MBR.
+      partitionTableType = "efi";
+      # We do not actually care much about the content of the partitions, so we
+      # do not need a bootloader installed.
+      installBootLoader = false;
+      # Improve determinism by not copying a channel.
+      copyChannel = false;
+    };
+  };
+in
+{
+  basic = makeTest {
+    name = "systemd-repart";
+    meta.maintainers = with maintainers; [ nikstur ];
+
+    nodes.machine = { config, pkgs, ... }: {
+      imports = [ common ];
+
+      boot.initrd.systemd.enable = true;
+
+      boot.initrd.systemd.repart.enable = true;
+      systemd.repart.partitions = {
+        "10-root" = {
+          Type = "linux-generic";
+        };
+      };
+    };
+
+    testScript = { nodes, ... }: ''
+      ${useDiskImage nodes.machine}
+
+      machine.start()
+      machine.wait_for_unit("multi-user.target")
+
+      systemd_repart_logs = machine.succeed("journalctl --boot --unit systemd-repart.service")
+      assert "Growing existing partition 1." in systemd_repart_logs
+    '';
+  };
+
+  after-initrd = makeTest {
+    name = "systemd-repart-after-initrd";
+    meta.maintainers = with maintainers; [ nikstur ];
+
+    nodes.machine = { config, pkgs, ... }: {
+      imports = [ common ];
+
+      systemd.repart.enable = true;
+      systemd.repart.partitions = {
+        "10-root" = {
+          Type = "linux-generic";
+        };
+      };
+    };
+
+    testScript = { nodes, ... }: ''
+      ${useDiskImage nodes.machine}
+
+      machine.start()
+      machine.wait_for_unit("multi-user.target")
+
+      systemd_repart_logs = machine.succeed("journalctl --unit systemd-repart.service")
+      assert "Growing existing partition 1." in systemd_repart_logs
+    '';
+  };
+}
diff --git a/nixos/tests/systemd-shutdown.nix b/nixos/tests/systemd-shutdown.nix
index 688cd6dd2c1..dad8167f198 100644
--- a/nixos/tests/systemd-shutdown.nix
+++ b/nixos/tests/systemd-shutdown.nix
@@ -11,6 +11,7 @@ in {
     systemd.shutdownRamfs.contents."/etc/systemd/system-shutdown/shutdown-message".source = pkgs.writeShellScript "shutdown-message" ''
       echo "${msg}"
     '';
+    boot.initrd.systemd.enable = systemdStage1;
   };
 
   testScript = ''
diff --git a/nixos/tests/systemd-user-tmpfiles-rules.nix b/nixos/tests/systemd-user-tmpfiles-rules.nix
new file mode 100644
index 00000000000..bf29b4b57be
--- /dev/null
+++ b/nixos/tests/systemd-user-tmpfiles-rules.nix
@@ -0,0 +1,35 @@
+import ./make-test-python.nix ({ lib, ... }: {
+  name = "systemd-user-tmpfiles-rules";
+
+  meta = with lib.maintainers; {
+    maintainers = [ schnusch ];
+  };
+
+  nodes.machine = { ... }: {
+    users.users = {
+      alice.isNormalUser = true;
+      bob.isNormalUser = true;
+    };
+
+    systemd.user.tmpfiles = {
+      rules = [
+        "d %h/user_tmpfiles_created"
+      ];
+      users.alice.rules = [
+        "d %h/only_alice"
+      ];
+    };
+  };
+
+  testScript = { ... }: ''
+    machine.succeed("loginctl enable-linger alice bob")
+
+    machine.wait_until_succeeds("systemctl --user --machine=alice@ is-active systemd-tmpfiles-setup.service")
+    machine.succeed("[ -d ~alice/user_tmpfiles_created ]")
+    machine.succeed("[ -d ~alice/only_alice ]")
+
+    machine.wait_until_succeeds("systemctl --user --machine=bob@ is-active systemd-tmpfiles-setup.service")
+    machine.succeed("[ -d ~bob/user_tmpfiles_created ]")
+    machine.succeed("[ ! -e ~bob/only_alice ]")
+  '';
+})
diff --git a/nixos/tests/systemd-userdbd.nix b/nixos/tests/systemd-userdbd.nix
new file mode 100644
index 00000000000..5d0233ffd9f
--- /dev/null
+++ b/nixos/tests/systemd-userdbd.nix
@@ -0,0 +1,32 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "systemd-userdbd";
+  nodes.machine = { config, pkgs, ... }: {
+    services.userdbd.enable = true;
+
+    users.users.test-user-nss = {
+      isNormalUser = true;
+    };
+
+    environment.etc."userdb/test-user-dropin.user".text = builtins.toJSON {
+      userName = "test-user-dropin";
+    };
+
+    environment.systemPackages = with pkgs; [ libvarlink ];
+  };
+  testScript = ''
+    import json
+    from shlex import quote
+
+    def getUserRecord(name):
+      Interface = "unix:/run/systemd/userdb/io.systemd.Multiplexer/io.systemd.UserDatabase"
+      payload = json.dumps({
+        "service": "io.systemd.Multiplexer",
+        "userName": name
+      })
+      return json.loads(machine.succeed(f"varlink call {Interface}.GetUserRecord {quote(payload)}"))
+
+    machine.wait_for_unit("systemd-userdbd.socket")
+    getUserRecord("test-user-nss")
+    getUserRecord("test-user-dropin")
+  '';
+})
diff --git a/nixos/tests/teleport.nix b/nixos/tests/teleport.nix
index 34bf1bc0c70..cdf762b1284 100644
--- a/nixos/tests/teleport.nix
+++ b/nixos/tests/teleport.nix
@@ -1,18 +1,28 @@
 { system ? builtins.currentSystem
 , config ? { }
 , pkgs ? import ../.. { inherit system config; }
+, lib ? pkgs.lib
 }:
 
 with import ../lib/testing-python.nix { inherit system pkgs; };
 
 let
-  minimal = { config, ... }: {
-    services.teleport.enable = true;
+  packages = with pkgs; {
+    "default" = teleport;
+    "11" = teleport_11;
   };
 
-  client = { config, ... }: {
+  minimal = package: {
     services.teleport = {
       enable = true;
+      inherit package;
+    };
+  };
+
+  client = package: {
+    services.teleport = {
+      enable = true;
+      inherit package;
       settings = {
         teleport = {
           nodename = "client";
@@ -37,9 +47,10 @@ let
     }];
   };
 
-  server = { config, ... }: {
+  server = package: {
     services.teleport = {
       enable = true;
+      inherit package;
       settings = {
         teleport = {
           nodename = "server";
@@ -64,36 +75,41 @@ let
     };
   };
 in
-{
-  minimal = makeTest {
-    # minimal setup should always work
-    name = "teleport-minimal-setup";
-    meta.maintainers = with pkgs.lib.maintainers; [ ymatsiuk ];
-    nodes = { inherit minimal; };
+lib.concatMapAttrs
+  (name: package: {
+    "minimal_${name}" = makeTest {
+      # minimal setup should always work
+      name = "teleport-minimal-setup";
+      meta.maintainers = with pkgs.lib.maintainers; [ justinas ];
+      nodes.minimal = minimal package;
 
-    testScript = ''
-      minimal.wait_for_open_port(3025)
-      minimal.wait_for_open_port(3080)
-      minimal.wait_for_open_port(3022)
-    '';
-  };
+      testScript = ''
+        minimal.wait_for_open_port(3025)
+        minimal.wait_for_open_port(3080)
+        minimal.wait_for_open_port(3022)
+      '';
+    };
 
-  basic = makeTest {
-    # basic server and client test
-    name = "teleport-server-client";
-    meta.maintainers = with pkgs.lib.maintainers; [ ymatsiuk ];
-    nodes = { inherit server client; };
+    "basic_${name}" = makeTest {
+      # basic server and client test
+      name = "teleport-server-client";
+      meta.maintainers = with pkgs.lib.maintainers; [ justinas ];
+      nodes = {
+        server = server package;
+        client = client package;
+      };
 
-    testScript = ''
-      with subtest("teleport ready"):
-          server.wait_for_open_port(3025)
-          client.wait_for_open_port(3022)
+      testScript = ''
+        with subtest("teleport ready"):
+            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)
-          client.succeed("journalctl -u teleport.service --grep='DEBU'")
-          server.succeed("journalctl -u teleport.service --grep='Starting teleport in insecure mode.'")
-    '';
-  };
-}
+        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)
+            client.succeed("journalctl -u teleport.service --grep='DEBU'")
+            server.succeed("journalctl -u teleport.service --grep='Starting teleport in insecure mode.'")
+      '';
+    };
+  })
+  packages
diff --git a/nixos/tests/timescaledb.nix b/nixos/tests/timescaledb.nix
new file mode 100644
index 00000000000..00a7f9af09f
--- /dev/null
+++ b/nixos/tests/timescaledb.nix
@@ -0,0 +1,93 @@
+# mostly copied from ./postgresql.nix as it seemed unapproriate to
+# test additional extensions for postgresql there.
+
+{ system ? builtins.currentSystem
+, config ? { }
+, pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  postgresql-versions = import ../../pkgs/servers/sql/postgresql pkgs;
+  test-sql = pkgs.writeText "postgresql-test" ''
+    CREATE EXTENSION timescaledb;
+    CREATE EXTENSION timescaledb_toolkit;
+
+    CREATE TABLE sth (
+      time TIMESTAMPTZ NOT NULL,
+      value DOUBLE PRECISION
+    );
+
+    SELECT create_hypertable('sth', 'time');
+
+    INSERT INTO sth (time, value) VALUES
+    ('2003-04-12 04:05:06 America/New_York', 1.0),
+    ('2003-04-12 04:05:07 America/New_York', 2.0),
+    ('2003-04-12 04:05:08 America/New_York', 3.0),
+    ('2003-04-12 04:05:09 America/New_York', 4.0),
+    ('2003-04-12 04:05:10 America/New_York', 5.0)
+    ;
+
+    WITH t AS (
+      SELECT
+        time_bucket('1 day'::interval, time) AS dt,
+        stats_agg(value) AS stats
+      FROM sth
+      GROUP BY time_bucket('1 day'::interval, time)
+    )
+    SELECT
+      average(stats)
+    FROM t;
+  '';
+  make-postgresql-test = postgresql-name: postgresql-package: makeTest {
+    name = postgresql-name;
+    meta = with pkgs.lib.maintainers; {
+      maintainers = [ typetetris ];
+    };
+
+    nodes.machine = { ... }:
+      {
+        services.postgresql = {
+          enable = true;
+          package = postgresql-package;
+          extraPlugins = with postgresql-package.pkgs; [
+            timescaledb
+            timescaledb_toolkit
+          ];
+          settings = { shared_preload_libraries = "timescaledb, timescaledb_toolkit"; };
+        };
+      };
+
+    testScript = ''
+      def check_count(statement, lines):
+          return 'test $(sudo -u postgres psql postgres -tAc "{}"|wc -l) -eq {}'.format(
+              statement, lines
+          )
+
+
+      machine.start()
+      machine.wait_for_unit("postgresql")
+
+      with subtest("Postgresql with extensions timescaledb and timescaledb_toolkit is available just after unit start"):
+          machine.succeed(
+              "sudo -u postgres psql -f ${test-sql}"
+          )
+
+      machine.fail(check_count("SELECT * FROM sth;", 3))
+      machine.succeed(check_count("SELECT * FROM sth;", 5))
+      machine.fail(check_count("SELECT * FROM sth;", 4))
+
+      machine.shutdown()
+    '';
+
+  };
+  applicablePostgresqlVersions = filterAttrs (_: value: versionAtLeast value.version "12") postgresql-versions;
+in
+mapAttrs'
+  (name: package: {
+    inherit name;
+    value = make-postgresql-test name package;
+  })
+  applicablePostgresqlVersions
diff --git a/nixos/tests/tracee.nix b/nixos/tests/tracee.nix
index 72e82ec0b7e..8ec86ef091e 100644
--- a/nixos/tests/tracee.nix
+++ b/nixos/tests/tracee.nix
@@ -1,37 +1,46 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "tracee-integration";
+  meta.maintainers = pkgs.tracee.meta.maintainers;
+
   nodes = {
     machine = { config, pkgs, ... }: {
-      # EventFilters/trace_only_events_from_new_containers requires docker
-      # podman with docker compat will suffice
-      virtualisation.podman.enable = true;
-      virtualisation.podman.dockerCompat = true;
+      # EventFilters/trace_only_events_from_new_containers and
+      # Test_EventFilters/trace_only_events_from_"dockerd"_binary_and_contain_it's_pid
+      # require docker/dockerd
+      virtualisation.docker.enable = true;
 
-      environment.systemPackages = [
+      environment.systemPackages = with pkgs; [
+        # required by Test_EventFilters/trace_events_from_ls_and_which_binary_in_separate_scopes
+        which
         # build the go integration tests as a binary
-        (pkgs.tracee.overrideAttrs (oa: {
+        (tracee.overrideAttrs (oa: {
           pname = oa.pname + "-integration";
-          patches = oa.patches or [] ++ [
-            # change the prefix from /usr/bin to /run to find nix processes
-            ../../pkgs/tools/security/tracee/test-EventFilters-prefix-nix-friendly.patch
-          ];
+          postPatch = oa.postPatch or "" + ''
+            # prepare tester.sh (which will be embedded in the test binary)
+            patchShebangs tests/integration/tester.sh
+
+            # fix the test to look at nixos paths for running programs
+            substituteInPlace tests/integration/integration_test.go \
+              --replace "bin=/usr/bin/" "comm=" \
+              --replace "binary=/usr/bin/" "comm=" \
+              --replace "/usr/bin/dockerd" "dockerd" \
+              --replace "/usr/bin" "/run/current-system/sw/bin"
+          '';
+          nativeBuildInputs = oa.nativeBuildInputs or [ ] ++ [ makeWrapper ];
           buildPhase = ''
             runHook preBuild
             # just build the static lib we need for the go test binary
-            make $makeFlags ''${enableParallelBuilding:+-j$NIX_BUILD_CORES -l$NIX_BUILD_CORES} bpf-core ./dist/btfhub
-
-            # remove the /usr/bin prefix to work with the patch above
-            substituteInPlace tests/integration/integration_test.go \
-              --replace "/usr/bin/ls" "ls"
+            make $makeFlags ''${enableParallelBuilding:+-j$NIX_BUILD_CORES} bpf-core ./dist/btfhub
 
             # then compile the tests to be ran later
             CGO_LDFLAGS="$(pkg-config --libs libbpf)" go test -tags core,ebpf,integration -p 1 -c -o $GOPATH/tracee-integration ./tests/integration/...
             runHook postBuild
           '';
           doCheck = false;
+          outputs = [ "out" ];
           installPhase = ''
             mkdir -p $out/bin
-            cp $GOPATH/tracee-integration $out/bin
+            mv $GOPATH/tracee-integration $out/bin/
           '';
           doInstallCheck = false;
         }))
@@ -40,10 +49,16 @@ import ./make-test-python.nix ({ pkgs, ... }: {
   };
 
   testScript = ''
+    machine.wait_for_unit("docker.service")
+
     with subtest("run integration tests"):
       # EventFilters/trace_only_events_from_new_containers also requires a container called "alpine"
-      machine.succeed('tar cv -C ${pkgs.pkgsStatic.busybox} . | podman import - alpine --change ENTRYPOINT=sleep')
+      machine.succeed('tar c -C ${pkgs.pkgsStatic.busybox} . | docker import - alpine --change "ENTRYPOINT [\"sleep\"]"')
 
-      print(machine.succeed('TRC_BIN="${pkgs.tracee}" tracee-integration -test.v'))
+      # Test_EventFilters/trace_event_set_in_a_specific_scope expects to be in a dir that includes "integration"
+      print(machine.succeed(
+        'mkdir /tmp/integration',
+        'cd /tmp/integration && tracee-integration -test.v'
+      ))
   '';
 })
diff --git a/nixos/tests/trafficserver.nix b/nixos/tests/trafficserver.nix
index 983ded4f172..e4557c6c50e 100644
--- a/nixos/tests/trafficserver.nix
+++ b/nixos/tests/trafficserver.nix
@@ -172,6 +172,7 @@ import ./make-test-python.nix ({ pkgs, ... }: {
         assert re.fullmatch(expected, out) is not None, "no matching logs"
 
         out = json.loads(ats.succeed(f"traffic_logstats -jf {access_log_path}"))
+        assert isinstance(out, dict)
         assert out["total"]["error.total"]["req"] == "0", "unexpected log stat"
   '';
 })
diff --git a/nixos/tests/turbovnc-headless-server.nix b/nixos/tests/turbovnc-headless-server.nix
index 1dbf9297c81..a155f9f907b 100644
--- a/nixos/tests/turbovnc-headless-server.nix
+++ b/nixos/tests/turbovnc-headless-server.nix
@@ -26,7 +26,7 @@ import ./make-test-python.nix ({ pkgs, lib, ... }: {
     # So that we can ssh into the VM, see e.g.
     # http://blog.patapon.info/nixos-local-vm/#accessing-the-vm-with-ssh
     services.openssh.enable = true;
-    services.openssh.permitRootLogin = "yes";
+    services.openssh.settings.PermitRootLogin = "yes";
     users.extraUsers.root.password = "";
     users.mutableUsers = false;
   };
diff --git a/nixos/tests/tuxguitar.nix b/nixos/tests/tuxguitar.nix
index 037f489e544..00833024bfe 100644
--- a/nixos/tests/tuxguitar.nix
+++ b/nixos/tests/tuxguitar.nix
@@ -1,7 +1,7 @@
 import ./make-test-python.nix ({ pkgs, ... }: {
   name = "tuxguitar";
   meta = with pkgs.lib.maintainers; {
-    maintainers = [ asbachb ];
+    maintainers = [ ];
   };
 
   nodes.machine = { config, pkgs, ... }: {
diff --git a/nixos/tests/txredisapi.nix b/nixos/tests/txredisapi.nix
index 7c6b36a5c47..47c2ba6d374 100644
--- a/nixos/tests/txredisapi.nix
+++ b/nixos/tests/txredisapi.nix
@@ -12,7 +12,7 @@ import ./make-test-python.nix ({ pkgs, ... }:
       {
         services.redis.servers."".enable = true;
 
-        environment.systemPackages = with pkgs; [ (python38.withPackages (ps: [ ps.twisted ps.txredisapi ps.mock ]))];
+        environment.systemPackages = with pkgs; [ (python3.withPackages (ps: [ ps.twisted ps.txredisapi ps.mock ]))];
       };
   };
 
diff --git a/nixos/tests/ulogd.nix b/nixos/tests/ulogd.nix
new file mode 100644
index 00000000000..ce52d855ffc
--- /dev/null
+++ b/nixos/tests/ulogd.nix
@@ -0,0 +1,84 @@
+import ./make-test-python.nix ({ pkgs, lib, ... }: {
+  name = "ulogd";
+
+  meta = with lib; {
+    maintainers = with maintainers; [ p-h ];
+  };
+
+  nodes.machine = { ... }: {
+    networking.firewall.enable = false;
+    networking.nftables.enable = true;
+    networking.nftables.ruleset = ''
+      table inet filter {
+        chain input {
+          type filter hook input priority 0;
+          log group 2 accept
+        }
+
+        chain output {
+          type filter hook output priority 0; policy accept;
+          log group 2 accept
+        }
+
+        chain forward {
+          type filter hook forward priority 0; policy drop;
+          log group 2 accept
+        }
+
+      }
+    '';
+    services.ulogd = {
+      enable = true;
+      settings = {
+        global = {
+          logfile = "/var/log/ulogd.log";
+          stack = "log1:NFLOG,base1:BASE,pcap1:PCAP";
+        };
+
+        log1.group = 2;
+
+        pcap1 = {
+          file = "/var/log/ulogd.pcap";
+          sync = 1;
+        };
+      };
+    };
+
+    environment.systemPackages = with pkgs; [
+      tcpdump
+    ];
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("ulogd.service")
+    machine.wait_for_unit("network-online.target")
+
+    with subtest("Ulogd is running"):
+        machine.succeed("pgrep ulogd >&2")
+
+    # All packets show up twice in the logs
+    with subtest("Logs are collected"):
+        machine.succeed("ping -f 127.0.0.1 -c 5 >&2")
+        machine.succeed("sleep 2")
+        machine.wait_until_succeeds("du /var/log/ulogd.pcap >&2")
+        _, echo_request_packets = machine.execute("tcpdump -r /var/log/ulogd.pcap icmp[0] == 8 and host 127.0.0.1")
+        expected, actual = 5*2, len(echo_request_packets.splitlines())
+        assert expected == actual, f"Expected {expected} packets, got: {actual}"
+        _, echo_reply_packets = machine.execute("tcpdump -r /var/log/ulogd.pcap icmp[0] == 0 and host 127.0.0.1")
+        expected, actual = 5*2, len(echo_reply_packets.splitlines())
+        assert expected == actual, f"Expected {expected} packets, got: {actual}"
+
+    with subtest("Reloading service reopens log file"):
+        machine.succeed("mv /var/log/ulogd.pcap /var/log/old_ulogd.pcap")
+        machine.succeed("systemctl reload ulogd.service")
+        machine.succeed("ping -f 127.0.0.1 -c 5 >&2")
+        machine.succeed("sleep 2")
+        _, echo_request_packets = machine.execute("tcpdump -r /var/log/ulogd.pcap icmp[0] == 8 and host 127.0.0.1")
+        expected, actual = 5*2, len(echo_request_packets.splitlines())
+        assert expected == actual, f"Expected {expected} packets, got: {actual}"
+        _, echo_reply_packets = machine.execute("tcpdump -r /var/log/ulogd.pcap icmp[0] == 0 and host 127.0.0.1")
+        expected, actual = 5*2, len(echo_reply_packets.splitlines())
+        assert expected == actual, f"Expected {expected} packets, got: {actual}"
+  '';
+})
diff --git a/nixos/tests/unifi.nix b/nixos/tests/unifi.nix
index 9dc7e5d04bd..d371bafd696 100644
--- a/nixos/tests/unifi.nix
+++ b/nixos/tests/unifi.nix
@@ -16,6 +16,8 @@ let
     };
 
     nodes.server = {
+      nixpkgs.config = config;
+
       services.unifi = {
         enable = true;
         unifiPackage = unifi;
diff --git a/nixos/tests/vault-agent.nix b/nixos/tests/vault-agent.nix
new file mode 100644
index 00000000000..dc86c829b67
--- /dev/null
+++ b/nixos/tests/vault-agent.nix
@@ -0,0 +1,52 @@
+import ./make-test-python.nix ({ pkgs, ... }: {
+  name = "vault-agent";
+
+  nodes.machine = { config, pkgs, ... }: {
+    services.vault-agent.instances.example.settings = {
+      vault.address = config.environment.variables.VAULT_ADDR;
+
+      auto_auth = [{
+        method = [{
+          type = "token_file";
+          config.token_file_path = pkgs.writeText "vault-token" config.environment.variables.VAULT_TOKEN;
+        }];
+      }];
+
+      template = [{
+        contents = ''
+          {{- with secret "secret/example" }}
+          {{ .Data.data.key }}"
+          {{- end }}
+        '';
+        perms = "0600";
+        destination = "/example";
+      }];
+    };
+
+    services.vault = {
+      enable = true;
+      dev = true;
+      devRootTokenID = config.environment.variables.VAULT_TOKEN;
+    };
+
+    environment = {
+      systemPackages = [ pkgs.vault ];
+      variables = {
+        VAULT_ADDR = "http://localhost:8200";
+        VAULT_TOKEN = "root";
+      };
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("vault.service")
+    machine.wait_for_open_port(8200)
+
+    machine.wait_until_succeeds('vault kv put secret/example key=example')
+
+    machine.wait_for_unit("vault-agent-example.service")
+
+    machine.wait_for_file("/example")
+    machine.succeed('grep "example" /example')
+  '';
+})
diff --git a/nixos/tests/vaultwarden.nix b/nixos/tests/vaultwarden.nix
index c0e1d0585b9..95d00c1d8ec 100644
--- a/nixos/tests/vaultwarden.nix
+++ b/nixos/tests/vaultwarden.nix
@@ -107,7 +107,7 @@ let
 
                   wait = WebDriverWait(driver, 10)
 
-                  wait.until(EC.title_contains("Create Account"))
+                  wait.until(EC.title_contains("Create account"))
 
                   driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_email').send_keys(
                       '${userEmail}'
@@ -121,19 +121,23 @@ let
                   driver.find_element(By.CSS_SELECTOR, 'input#register-form_input_confirm-master-password').send_keys(
                       '${userPassword}'
                   )
+                  if driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').is_selected():
+                      driver.find_element(By.CSS_SELECTOR, 'input#checkForBreaches').click()
 
-                  driver.find_element(By.XPATH, "//button[contains(., 'Create Account')]").click()
+                  driver.find_element(By.XPATH, "//button[contains(., 'Create account')]").click()
 
-                  wait.until_not(EC.title_contains("Create Account"))
+                  wait.until_not(EC.title_contains("Create account"))
+
+                  driver.find_element(By.XPATH, "//button[contains(., 'Continue')]").click()
 
                   driver.find_element(By.CSS_SELECTOR, 'input#login_input_master-password').send_keys(
                       '${userPassword}'
                   )
-                  driver.find_element(By.XPATH, "//button[contains(., 'Log In')]").click()
+                  driver.find_element(By.XPATH, "//button[contains(., 'Log in')]").click()
 
-                  wait.until(EC.title_contains("Bitwarden Web Vault"))
+                  wait.until(EC.title_contains("Vaults"))
 
-                  driver.find_element(By.XPATH, "//button[contains(., 'Add Item')]").click()
+                  driver.find_element(By.XPATH, "//button[contains(., 'New item')]").click()
 
                   driver.find_element(By.CSS_SELECTOR, 'input#name').send_keys(
                       'secrets'
diff --git a/nixos/tests/vscodium.nix b/nixos/tests/vscodium.nix
index ee884cc4295..3eda8b6cfb2 100644
--- a/nixos/tests/vscodium.nix
+++ b/nixos/tests/vscodium.nix
@@ -33,13 +33,6 @@ let
       };
       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():
@@ -49,11 +42,11 @@ let
         start_all()
 
         machine.wait_for_unit('graphical.target')
-        machine.wait_until_succeeds('pgrep -x codium')
 
-        with codium_running:
+        codium_running.wait() # type: ignore[union-attr]
+        with codium_running: # type: ignore[union-attr]
             # Wait until vscodium is visible. "File" is in the menu bar.
-            machine.wait_for_text('Get Started')
+            machine.wait_for_text('Welcome')
             machine.screenshot('start_screen')
 
             test_string = 'testfile'
diff --git a/nixos/tests/web-apps/mastodon/script.nix b/nixos/tests/web-apps/mastodon/script.nix
index cdb1d4379b6..a89b4b7480e 100644
--- a/nixos/tests/web-apps/mastodon/script.nix
+++ b/nixos/tests/web-apps/mastodon/script.nix
@@ -9,7 +9,7 @@
   ${extraInit}
 
   server.wait_for_unit("redis-mastodon.service")
-  server.wait_for_unit("mastodon-sidekiq.service")
+  server.wait_for_unit("mastodon-sidekiq-all.service")
   server.wait_for_unit("mastodon-streaming.service")
   server.wait_for_unit("mastodon-web.service")
   server.wait_for_open_port(55000)
diff --git a/nixos/tests/web-apps/monica.nix b/nixos/tests/web-apps/monica.nix
new file mode 100644
index 00000000000..29f5cb85bb1
--- /dev/null
+++ b/nixos/tests/web-apps/monica.nix
@@ -0,0 +1,33 @@
+import ../make-test-python.nix ({pkgs, ...}:
+let
+  cert = pkgs.runCommand "selfSignedCerts" { nativeBuildInputs = [ pkgs.openssl ]; } ''
+    openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=localhost' -days 36500
+    mkdir -p $out
+    cp key.pem cert.pem $out
+  '';
+in
+{
+  name = "monica";
+
+  nodes = {
+    machine = {pkgs, ...}: {
+      services.monica = {
+        enable = true;
+        hostname = "localhost";
+        appKeyFile = "${pkgs.writeText "keyfile" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}";
+        nginx = {
+          forceSSL = true;
+          sslCertificate = "${cert}/cert.pem";
+          sslCertificateKey = "${cert}/key.pem";
+        };
+      };
+    };
+  };
+
+  testScript = ''
+    start_all()
+    machine.wait_for_unit("monica-setup.service")
+    machine.wait_for_open_port(443)
+    machine.succeed("curl -k --fail https://localhost", timeout=10)
+  '';
+})
diff --git a/nixos/tests/web-apps/netbox.nix b/nixos/tests/web-apps/netbox.nix
index 35decdd49e8..30de74f1886 100644
--- a/nixos/tests/web-apps/netbox.nix
+++ b/nixos/tests/web-apps/netbox.nix
@@ -1,21 +1,146 @@
-import ../make-test-python.nix ({ lib, pkgs, ... }: {
+let
+  ldapDomain = "example.org";
+  ldapSuffix = "dc=example,dc=org";
+
+  ldapRootUser = "admin";
+  ldapRootPassword = "foobar";
+
+  testUser = "alice";
+  testPassword = "verySecure";
+  testGroup = "netbox-users";
+in import ../make-test-python.nix ({ lib, pkgs, netbox, ... }: {
   name = "netbox";
 
   meta = with lib.maintainers; {
-    maintainers = [ n0emis ];
+    maintainers = [ minijackson n0emis ];
   };
 
-  nodes.machine = { ... }: {
+  nodes.machine = { config, ... }: {
     services.netbox = {
       enable = true;
+      package = netbox;
       secretKeyFile = pkgs.writeText "secret" ''
         abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
       '';
+
+      enableLdap = true;
+      ldapConfigPath = pkgs.writeText "ldap_config.py" ''
+        import ldap
+        from django_auth_ldap.config import LDAPSearch, PosixGroupType
+
+        AUTH_LDAP_SERVER_URI = "ldap://localhost/"
+
+        AUTH_LDAP_USER_SEARCH = LDAPSearch(
+            "ou=accounts,ou=posix,${ldapSuffix}",
+            ldap.SCOPE_SUBTREE,
+            "(uid=%(user)s)",
+        )
+
+        AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
+            "ou=groups,ou=posix,${ldapSuffix}",
+            ldap.SCOPE_SUBTREE,
+            "(objectClass=posixGroup)",
+        )
+        AUTH_LDAP_GROUP_TYPE = PosixGroupType()
+
+        # Mirror LDAP group assignments.
+        AUTH_LDAP_MIRROR_GROUPS = True
+
+        # For more granular permissions, we can map LDAP groups to Django groups.
+        AUTH_LDAP_FIND_GROUP_PERMS = True
+      '';
+    };
+
+    services.nginx = {
+      enable = true;
+
+      recommendedProxySettings = true;
+
+      virtualHosts.netbox = {
+        default = true;
+        locations."/".proxyPass = "http://localhost:${toString config.services.netbox.port}";
+        locations."/static/".alias = "/var/lib/netbox/static/";
+      };
+    };
+
+    # Adapted from the sssd-ldap NixOS test
+    services.openldap = {
+      enable = true;
+      settings = {
+        children = {
+          "cn=schema".includes = [
+            "${pkgs.openldap}/etc/schema/core.ldif"
+            "${pkgs.openldap}/etc/schema/cosine.ldif"
+            "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+            "${pkgs.openldap}/etc/schema/nis.ldif"
+          ];
+          "olcDatabase={1}mdb" = {
+            attrs = {
+              objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+              olcDatabase = "{1}mdb";
+              olcDbDirectory = "/var/lib/openldap/db";
+              olcSuffix = ldapSuffix;
+              olcRootDN = "cn=${ldapRootUser},${ldapSuffix}";
+              olcRootPW = ldapRootPassword;
+            };
+          };
+        };
+      };
+      declarativeContents = {
+        ${ldapSuffix} = ''
+          dn: ${ldapSuffix}
+          objectClass: top
+          objectClass: dcObject
+          objectClass: organization
+          o: ${ldapDomain}
+
+          dn: ou=posix,${ldapSuffix}
+          objectClass: top
+          objectClass: organizationalUnit
+
+          dn: ou=accounts,ou=posix,${ldapSuffix}
+          objectClass: top
+          objectClass: organizationalUnit
+
+          dn: uid=${testUser},ou=accounts,ou=posix,${ldapSuffix}
+          objectClass: person
+          objectClass: posixAccount
+          userPassword: ${testPassword}
+          homeDirectory: /home/${testUser}
+          uidNumber: 1234
+          gidNumber: 1234
+          cn: ""
+          sn: ""
+
+          dn: ou=groups,ou=posix,${ldapSuffix}
+          objectClass: top
+          objectClass: organizationalUnit
+
+          dn: cn=${testGroup},ou=groups,ou=posix,${ldapSuffix}
+          objectClass: posixGroup
+          gidNumber: 2345
+          memberUid: ${testUser}
+        '';
+      };
     };
+
+    users.users.nginx.extraGroups = [ "netbox" ];
+
+    networking.firewall.allowedTCPPorts = [ 80 ];
   };
 
-  testScript = ''
-    machine.start()
+  testScript = let
+    changePassword = pkgs.writeText "change-password.py" ''
+      from django.contrib.auth.models import User
+      u = User.objects.get(username='netbox')
+      u.set_password('netbox')
+      u.save()
+    '';
+  in ''
+    from typing import Any, Dict
+    import json
+
+    start_all()
     machine.wait_for_unit("netbox.target")
     machine.wait_until_succeeds("journalctl --since -1m --unit netbox --grep Listening")
 
@@ -26,5 +151,167 @@ import ../make-test-python.nix ({ lib, pkgs, ... }: {
 
     with subtest("Staticfiles are generated"):
         machine.succeed("test -e /var/lib/netbox/static/netbox.js")
+
+    with subtest("Superuser can be created"):
+        machine.succeed(
+            "netbox-manage createsuperuser --noinput --username netbox --email netbox@example.com"
+        )
+        # Django doesn't have a "clean" way of inputting the password from the command line
+        machine.succeed("cat '${changePassword}' | netbox-manage shell")
+
+    machine.wait_for_unit("network.target")
+
+    with subtest("Home screen loads from nginx"):
+        machine.succeed(
+            "curl -sSfL http://localhost | grep '<title>Home | NetBox</title>'"
+        )
+
+    with subtest("Staticfiles can be fetched"):
+        machine.succeed("curl -sSfL http://localhost/static/netbox.js")
+        machine.succeed("curl -sSfL http://localhost/static/docs/")
+
+    with subtest("Can interact with API"):
+        json.loads(
+            machine.succeed("curl -sSfL -H 'Accept: application/json' 'http://localhost/api/'")
+        )
+
+    def login(username: str, password: str):
+        encoded_data = json.dumps({"username": username, "password": password})
+        uri = "/users/tokens/provision/"
+        result = json.loads(
+            machine.succeed(
+                "curl -sSfL "
+                "-X POST "
+                "-H 'Accept: application/json' "
+                "-H 'Content-Type: application/json' "
+                f"'http://localhost/api{uri}' "
+                f"--data '{encoded_data}'"
+            )
+        )
+        return result["key"]
+
+    with subtest("Can login"):
+        auth_token = login("netbox", "netbox")
+
+    def get(uri: str):
+        return json.loads(
+            machine.succeed(
+                "curl -sSfL "
+                "-H 'Accept: application/json' "
+                f"-H 'Authorization: Token {auth_token}' "
+                f"'http://localhost/api{uri}'"
+            )
+        )
+
+    def delete(uri: str):
+        return machine.succeed(
+            "curl -sSfL "
+            f"-X DELETE "
+            "-H 'Accept: application/json' "
+            f"-H 'Authorization: Token {auth_token}' "
+            f"'http://localhost/api{uri}'"
+        )
+
+
+    def data_request(uri: str, method: str, data: Dict[str, Any]):
+        encoded_data = json.dumps(data)
+        return json.loads(
+            machine.succeed(
+                "curl -sSfL "
+                f"-X {method} "
+                "-H 'Accept: application/json' "
+                "-H 'Content-Type: application/json' "
+                f"-H 'Authorization: Token {auth_token}' "
+                f"'http://localhost/api{uri}' "
+                f"--data '{encoded_data}'"
+            )
+        )
+
+    def post(uri: str, data: Dict[str, Any]):
+      return data_request(uri, "POST", data)
+
+    def patch(uri: str, data: Dict[str, Any]):
+      return data_request(uri, "PATCH", data)
+
+    with subtest("Can create objects"):
+        result = post("/dcim/sites/", {"name": "Test site", "slug": "test-site"})
+        site_id = result["id"]
+
+        # Example from:
+        # http://netbox.extra.cea.fr/static/docs/integrations/rest-api/#creating-a-new-object
+        post("/ipam/prefixes/", {"prefix": "192.0.2.0/24", "site": site_id})
+
+        result = post(
+            "/dcim/manufacturers/",
+            {"name": "Test manufacturer", "slug": "test-manufacturer"}
+        )
+        manufacturer_id = result["id"]
+
+        # Had an issue with device-types before NetBox 3.4.0
+        result = post(
+            "/dcim/device-types/",
+            {
+                "model": "Test device type",
+                "manufacturer": manufacturer_id,
+                "slug": "test-device-type",
+            },
+        )
+        device_type_id = result["id"]
+
+    with subtest("Can list objects"):
+        result = get("/dcim/sites/")
+
+        assert result["count"] == 1
+        assert result["results"][0]["id"] == site_id
+        assert result["results"][0]["name"] == "Test site"
+        assert result["results"][0]["description"] == ""
+
+        result = get("/dcim/device-types/")
+        assert result["count"] == 1
+        assert result["results"][0]["id"] == device_type_id
+        assert result["results"][0]["model"] == "Test device type"
+
+    with subtest("Can update objects"):
+        new_description = "Test site description"
+        patch(f"/dcim/sites/{site_id}/", {"description": new_description})
+        result = get(f"/dcim/sites/{site_id}/")
+        assert result["description"] == new_description
+
+    with subtest("Can delete objects"):
+        # Delete a device-type since no object depends on it
+        delete(f"/dcim/device-types/{device_type_id}/")
+
+        result = get("/dcim/device-types/")
+        assert result["count"] == 0
+
+    with subtest("Can use the GraphQL API"):
+        encoded_data = json.dumps({
+            "query": "query { prefix_list { prefix, site { id, description } } }",
+        })
+        result = json.loads(
+            machine.succeed(
+                "curl -sSfL "
+                "-H 'Accept: application/json' "
+                "-H 'Content-Type: application/json' "
+                f"-H 'Authorization: Token {auth_token}' "
+                "'http://localhost/graphql/' "
+                f"--data '{encoded_data}'"
+            )
+        )
+
+        assert len(result["data"]["prefix_list"]) == 1
+        assert result["data"]["prefix_list"][0]["prefix"] == "192.0.2.0/24"
+        assert result["data"]["prefix_list"][0]["site"]["id"] == str(site_id)
+        assert result["data"]["prefix_list"][0]["site"]["description"] == new_description
+
+    with subtest("Can login with LDAP"):
+        machine.wait_for_unit("openldap.service")
+        login("alice", "${testPassword}")
+
+    with subtest("Can associate LDAP groups"):
+        result = get("/users/users/?username=${testUser}")
+
+        assert result["count"] == 1
+        assert any(group["name"] == "${testGroup}" for group in result["results"][0]["groups"])
   '';
 })
diff --git a/nixos/tests/web-apps/peertube.nix b/nixos/tests/web-apps/peertube.nix
index ecc45bff2e2..0e5f39c08a0 100644
--- a/nixos/tests/web-apps/peertube.nix
+++ b/nixos/tests/web-apps/peertube.nix
@@ -41,6 +41,9 @@ import ../make-test-python.nix ({pkgs, ...}:
     server = { pkgs, ... }: {
       environment = {
         etc = {
+          "peertube/secrets-peertube".text = ''
+            063d9c60d519597acef26003d5ecc32729083965d09181ef3949200cbe5f09ee
+          '';
           "peertube/password-posgressql-db".text = ''
             0gUN0C1mgST6czvjZ8T9
           '';
@@ -67,6 +70,10 @@ import ../make-test-python.nix ({pkgs, ...}:
         localDomain = "peertube.local";
         enableWebHttps = false;
 
+        secrets = {
+          secretsFile = "/etc/peertube/secrets-peertube";
+        };
+
         database = {
           host = "192.168.2.10";
           name = "peertube_local";
diff --git a/nixos/tests/web-apps/snipe-it.nix b/nixos/tests/web-apps/snipe-it.nix
new file mode 100644
index 00000000000..123d7742056
--- /dev/null
+++ b/nixos/tests/web-apps/snipe-it.nix
@@ -0,0 +1,101 @@
+/*
+Snipe-IT NixOS test
+
+It covers the following scenario:
+- Installation
+- Backup and restore
+
+Scenarios NOT covered by this test (but perhaps in the future):
+- Sending and receiving emails
+*/
+{ pkgs, ... }: let
+  siteName = "NixOS Snipe-IT Test Instance";
+in {
+  name = "snipe-it";
+
+  meta.maintainers = with pkgs.lib.maintainers; [ yayayayaka ];
+
+  nodes = {
+    snipeit = { ... }: {
+      services.snipe-it = {
+        enable = true;
+        appKeyFile = toString (pkgs.writeText "snipe-it-app-key" "uTqGUN5GUmUrh/zSAYmhyzRk62pnpXICyXv9eeITI8k=");
+        hostName = "localhost";
+        database.createLocally = true;
+        mail = {
+          driver = "smtp";
+          encryption = "tls";
+          host = "localhost";
+          port = 1025;
+          from.name = "Snipe-IT NixOS test";
+          from.address = "snipe-it@localhost";
+          replyTo.address = "snipe-it@localhost";
+          user = "snipe-it@localhost";
+          passwordFile = toString (pkgs.writeText "snipe-it-mail-pass" "a-secure-mail-password");
+        };
+      };
+    };
+  };
+
+  testScript = { nodes }: let
+    backupPath = "${nodes.snipeit.services.snipe-it.dataDir}/storage/app/backups";
+
+    # Snipe-IT has been installed successfully if the site name shows up on the login page
+    checkLoginPage = { shouldSucceed ? true }: ''
+      snipeit.${if shouldSucceed then "succeed" else "fail"}("""curl http://localhost/login | grep '${siteName}'""")
+    '';
+  in ''
+    start_all()
+
+    snipeit.wait_for_unit("nginx.service")
+    snipeit.wait_for_unit("snipe-it-setup.service")
+
+    # Create an admin user
+    snipeit.succeed(
+        """
+        snipe-it snipeit:create-admin \
+            --username="admin" \
+            --email="janedoe@localhost" \
+            --password="extremesecurepassword" \
+            --first_name="Jane" \
+            --last_name="Doe"
+        """
+    )
+
+    with subtest("Circumvent the pre-flight setup by just writing some settings into the database ourself"):
+        snipeit.succeed(
+            """
+            mysql -D ${nodes.snipeit.services.snipe-it.database.name} -e "INSERT INTO settings (id, user_id, site_name) VALUES ('1', '1', '${siteName}');"
+            """
+        )
+
+        # Usually these are generated during the pre-flight setup
+        snipeit.succeed("snipe-it passport:keys")
+
+
+    # Login page should now contain the configured site name
+    ${checkLoginPage {}}
+
+    with subtest("Test Backup and restore"):
+        snipeit.succeed("snipe-it snipeit:backup")
+
+        # One zip file should have been created
+        snipeit.succeed("""[ "$(ls -1 "${backupPath}" | wc -l)" -eq 1 ]""")
+
+        # Purge the state
+        snipeit.succeed("snipe-it migrate:fresh --force")
+
+        # Login page should disappear
+        ${checkLoginPage { shouldSucceed = false; }}
+
+        # Restore the state
+        snipeit.succeed(
+            """
+            snipe-it snipeit:restore --force $(find "${backupPath}/" -type f -name "*.zip")
+            """
+        )
+
+        # Login page should be back again
+        ${checkLoginPage {}}
+  '';
+}
diff --git a/nixos/tests/web-servers/stargazer.nix b/nixos/tests/web-servers/stargazer.nix
new file mode 100644
index 00000000000..c522cfee5db
--- /dev/null
+++ b/nixos/tests/web-servers/stargazer.nix
@@ -0,0 +1,31 @@
+{ pkgs, lib, ... }:
+{
+  name = "stargazer";
+  meta = with lib.maintainers; { maintainers = [ gaykitty ]; };
+
+  nodes = {
+    geminiserver = { pkgs, ... }: {
+      services.stargazer = {
+        enable = true;
+        routes = [
+          {
+            route = "localhost";
+            root = toString (pkgs.writeTextDir "index.gmi" ''
+              # Hello NixOS!
+            '');
+          }
+        ];
+      };
+    };
+  };
+
+  testScript = { nodes, ... }: ''
+    geminiserver.wait_for_unit("stargazer")
+    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/webhook.nix b/nixos/tests/webhook.nix
new file mode 100644
index 00000000000..ed705140864
--- /dev/null
+++ b/nixos/tests/webhook.nix
@@ -0,0 +1,65 @@
+{ pkgs, ... }:
+let
+  forwardedPort = 19000;
+  internalPort = 9000;
+in
+{
+  name = "webhook";
+
+  nodes = {
+    webhookMachine = { pkgs, ... }: {
+      virtualisation.forwardPorts = [{
+        host.port = forwardedPort;
+        guest.port = internalPort;
+      }];
+      services.webhook = {
+        enable = true;
+        port = internalPort;
+        openFirewall = true;
+        hooks = {
+          echo = {
+            execute-command = "echo";
+            response-message = "Webhook is reachable!";
+          };
+        };
+        hooksTemplated = {
+          echoTemplate = ''
+            {
+              "id": "echo-template",
+              "execute-command": "echo",
+              "response-message": "{{ getenv "WEBHOOK_MESSAGE" }}"
+            }
+          '';
+        };
+        environment.WEBHOOK_MESSAGE = "Templates are working!";
+      };
+    };
+  };
+
+  extraPythonPackages = p: [
+    p.requests
+    p.types-requests
+  ];
+
+  testScript = { nodes, ... }: ''
+    import requests
+    webhookMachine.wait_for_unit("webhook")
+    webhookMachine.wait_for_open_port(${toString internalPort})
+
+    with subtest("Check that webhooks can be called externally"):
+      response = requests.get("http://localhost:${toString forwardedPort}/hooks/echo")
+      print(f"Response code: {response.status_code}")
+      print("Response: %r" % response.content)
+
+      assert response.status_code == 200
+      assert response.content == b"Webhook is reachable!"
+
+    with subtest("Check that templated webhooks can be called externally"):
+      response = requests.get("http://localhost:${toString forwardedPort}/hooks/echo-template")
+      print(f"Response code: {response.status_code}")
+      print("Response: %r" % response.content)
+
+      assert response.status_code == 200
+      assert response.content == b"Templates are working!"
+  '';
+}
diff --git a/nixos/tests/wireguard/basic.nix b/nixos/tests/wireguard/basic.nix
index 36ab226cde0..96b0a681c36 100644
--- a/nixos/tests/wireguard/basic.nix
+++ b/nixos/tests/wireguard/basic.nix
@@ -1,5 +1,4 @@
-{ kernelPackages ? null }:
-import ../make-test-python.nix ({ pkgs, lib, ...} :
+import ../make-test-python.nix ({ pkgs, lib, kernelPackages ? null, ...} :
   let
     wg-snakeoil-keys = import ./snakeoil-keys.nix;
     peer = (import ./make-peer.nix) { inherit lib; };
diff --git a/nixos/tests/wireguard/default.nix b/nixos/tests/wireguard/default.nix
index dedb321ff2e..c30f1b74770 100644
--- a/nixos/tests/wireguard/default.nix
+++ b/nixos/tests/wireguard/default.nix
@@ -7,10 +7,11 @@
 with pkgs.lib;
 
 let
-  tests = let callTest = p: flip (import p) { inherit system pkgs; }; in {
+  tests = let callTest = p: args: import p ({ inherit system pkgs; } // args); in {
     basic = callTest ./basic.nix;
     namespaces = callTest ./namespaces.nix;
     wg-quick = callTest ./wg-quick.nix;
+    wg-quick-nftables = args: callTest ./wg-quick.nix ({ nftables = true; } // args);
     generated = callTest ./generated.nix;
   };
 in
diff --git a/nixos/tests/wireguard/generated.nix b/nixos/tests/wireguard/generated.nix
index 84a35d29b45..c58f7a75071 100644
--- a/nixos/tests/wireguard/generated.nix
+++ b/nixos/tests/wireguard/generated.nix
@@ -1,5 +1,4 @@
-{ kernelPackages ? null }:
-import ../make-test-python.nix ({ pkgs, lib, ... } : {
+import ../make-test-python.nix ({ pkgs, lib, kernelPackages ? null, ... } : {
   name = "wireguard-generated";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ ma27 grahamc ];
diff --git a/nixos/tests/wireguard/namespaces.nix b/nixos/tests/wireguard/namespaces.nix
index 93dc84a8768..d0eb009e110 100644
--- a/nixos/tests/wireguard/namespaces.nix
+++ b/nixos/tests/wireguard/namespaces.nix
@@ -1,5 +1,3 @@
-{ kernelPackages ? null }:
-
 let
   listenPort = 12345;
   socketNamespace = "foo";
@@ -15,7 +13,7 @@ let
 
 in
 
-import ../make-test-python.nix ({ pkgs, lib, ... } : {
+import ../make-test-python.nix ({ pkgs, lib, kernelPackages ? null, ... } : {
   name = "wireguard-with-namespaces";
   meta = with pkgs.lib.maintainers; {
     maintainers = [ asymmetric ];
@@ -41,6 +39,7 @@ import ../make-test-python.nix ({ pkgs, lib, ... } : {
         preSetup = ''
           ip netns add ${interfaceNamespace}
         '';
+        mtu = 1280;
         inherit interfaceNamespace;
       };
     };
diff --git a/nixos/tests/wireguard/snakeoil-keys.nix b/nixos/tests/wireguard/snakeoil-keys.nix
index 55ad582d405..c979f0e0c8a 100644
--- a/nixos/tests/wireguard/snakeoil-keys.nix
+++ b/nixos/tests/wireguard/snakeoil-keys.nix
@@ -6,6 +6,7 @@
 
   peer1 = {
     privateKey = "uO8JVo/sanx2DOM0L9GUEtzKZ82RGkRnYgpaYc7iXmg=";
-    publicKey = "Ks9yRJIi/0vYgRmn14mIOQRwkcUGBujYINbMpik2SBI=";
+    # readFile'd keys may have trailing newlines, emulate this
+    publicKey = "Ks9yRJIi/0vYgRmn14mIOQRwkcUGBujYINbMpik2SBI=\n";
   };
 }
diff --git a/nixos/tests/wireguard/wg-quick.nix b/nixos/tests/wireguard/wg-quick.nix
index bc2cba91188..ec2b8d7f2d9 100644
--- a/nixos/tests/wireguard/wg-quick.nix
+++ b/nixos/tests/wireguard/wg-quick.nix
@@ -1,9 +1,13 @@
-{ kernelPackages ? null }:
-
-import ../make-test-python.nix ({ pkgs, lib, ... }:
+import ../make-test-python.nix ({ pkgs, lib, kernelPackages ? null, nftables ? false, ... }:
   let
     wg-snakeoil-keys = import ./snakeoil-keys.nix;
-    peer = (import ./make-peer.nix) { inherit lib; };
+    peer = import ./make-peer.nix { inherit lib; };
+    commonConfig = {
+      boot.kernelPackages = lib.mkIf (kernelPackages != null) kernelPackages;
+      networking.nftables.enable = nftables;
+      # Make sure iptables doesn't work with nftables enabled
+      boot.blacklistedKernelModules = lib.mkIf nftables [ "nft_compat" ];
+    };
   in
   {
     name = "wg-quick";
@@ -15,47 +19,51 @@ import ../make-test-python.nix ({ pkgs, lib, ... }:
       peer0 = peer {
         ip4 = "192.168.0.1";
         ip6 = "fd00::1";
-        extraConfig = {
-          boot = lib.mkIf (kernelPackages != null) { inherit kernelPackages; };
-          networking.firewall.allowedUDPPorts = [ 23542 ];
-          networking.wg-quick.interfaces.wg0 = {
-            address = [ "10.23.42.1/32" "fc00::1/128" ];
-            listenPort = 23542;
+        extraConfig = lib.mkMerge [
+          commonConfig
+          {
+            networking.firewall.allowedUDPPorts = [ 23542 ];
+            networking.wg-quick.interfaces.wg0 = {
+              address = [ "10.23.42.1/32" "fc00::1/128" ];
+              listenPort = 23542;
 
-            inherit (wg-snakeoil-keys.peer0) privateKey;
+              inherit (wg-snakeoil-keys.peer0) privateKey;
 
-            peers = lib.singleton {
-              allowedIPs = [ "10.23.42.2/32" "fc00::2/128" ];
+              peers = lib.singleton {
+                allowedIPs = [ "10.23.42.2/32" "fc00::2/128" ];
 
-              inherit (wg-snakeoil-keys.peer1) publicKey;
-            };
+                inherit (wg-snakeoil-keys.peer1) publicKey;
+              };
 
-            dns = [ "10.23.42.2" "fc00::2" "wg0" ];
-          };
-        };
+              dns = [ "10.23.42.2" "fc00::2" "wg0" ];
+            };
+          }
+        ];
       };
 
       peer1 = peer {
         ip4 = "192.168.0.2";
         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;
+        extraConfig = lib.mkMerge [
+          commonConfig
+          {
+            networking.useNetworkd = true;
+            networking.wg-quick.interfaces.wg0 = {
+              address = [ "10.23.42.2/32" "fc00::2/128" ];
+              inherit (wg-snakeoil-keys.peer1) privateKey;
 
-            peers = lib.singleton {
-              allowedIPs = [ "0.0.0.0/0" "::/0" ];
-              endpoint = "192.168.0.1:23542";
-              persistentKeepalive = 25;
+              peers = lib.singleton {
+                allowedIPs = [ "0.0.0.0/0" "::/0" ];
+                endpoint = "192.168.0.1:23542";
+                persistentKeepalive = 25;
 
-              inherit (wg-snakeoil-keys.peer0) publicKey;
-            };
+                inherit (wg-snakeoil-keys.peer0) publicKey;
+              };
 
-            dns = [ "10.23.42.1" "fc00::1" "wg0" ];
-          };
-        };
+              dns = [ "10.23.42.1" "fc00::1" "wg0" ];
+            };
+          }
+        ];
       };
     };
 
diff --git a/nixos/tests/wordpress.nix b/nixos/tests/wordpress.nix
index 416a20aa7fe..4e322774fef 100644
--- a/nixos/tests/wordpress.nix
+++ b/nixos/tests/wordpress.nix
@@ -1,6 +1,6 @@
-import ./make-test-python.nix ({ pkgs, ... }:
+import ./make-test-python.nix ({ lib, pkgs, ... }:
 
-{
+rec {
   name = "wordpress";
   meta = with pkgs.lib.maintainers; {
     maintainers = [
@@ -10,17 +10,22 @@ import ./make-test-python.nix ({ pkgs, ... }:
     ];
   };
 
-  nodes = {
-    wp_httpd = { ... }: {
+  nodes = lib.foldl (a: version: let
+    package = pkgs."wordpress${version}";
+  in a // {
+    "wp${version}_httpd" = _: {
       services.httpd.adminAddr = "webmaster@site.local";
       services.httpd.logPerVirtualHost = true;
 
+      services.wordpress.webserver = "httpd";
       services.wordpress.sites = {
         "site1.local" = {
           database.tablePrefix = "site1_";
+          inherit package;
         };
         "site2.local" = {
           database.tablePrefix = "site2_";
+          inherit package;
         };
       };
 
@@ -28,14 +33,16 @@ import ./make-test-python.nix ({ pkgs, ... }:
       networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
     };
 
-    wp_nginx = { ... }: {
+    "wp${version}_nginx" = _: {
       services.wordpress.webserver = "nginx";
       services.wordpress.sites = {
         "site1.local" = {
           database.tablePrefix = "site1_";
+          inherit package;
         };
         "site2.local" = {
           database.tablePrefix = "site2_";
+          inherit package;
         };
       };
 
@@ -43,34 +50,38 @@ import ./make-test-python.nix ({ pkgs, ... }:
       networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
     };
 
-    wp_caddy = { ... }: {
+    "wp${version}_caddy" = _: {
       services.wordpress.webserver = "caddy";
       services.wordpress.sites = {
         "site1.local" = {
           database.tablePrefix = "site1_";
+          inherit package;
         };
         "site2.local" = {
           database.tablePrefix = "site2_";
+          inherit package;
         };
       };
 
       networking.firewall.allowedTCPPorts = [ 80 ];
       networking.hosts."127.0.0.1" = [ "site1.local" "site2.local" ];
     };
-  };
+  }) {} [
+    "6_1" "6_2"
+  ];
 
   testScript = ''
     import re
 
     start_all()
 
-    wp_httpd.wait_for_unit("httpd")
-    wp_nginx.wait_for_unit("nginx")
-    wp_caddy.wait_for_unit("caddy")
+    ${lib.concatStrings (lib.mapAttrsToList (name: value: ''
+      ${name}.wait_for_unit("${(value null).services.wordpress.webserver}")
+    '') nodes)}
 
     site_names = ["site1.local", "site2.local"]
 
-    for machine in (wp_httpd, wp_nginx, wp_caddy):
+    for machine in (${lib.concatStringsSep ", " (builtins.attrNames nodes)}):
         for site_name in site_names:
             machine.wait_for_unit(f"phpfpm-wordpress-{site_name}")
 
diff --git a/nixos/tests/xfce.nix b/nixos/tests/xfce.nix
index 31f00f77c40..3758ccbccf4 100644
--- a/nixos/tests/xfce.nix
+++ b/nixos/tests/xfce.nix
@@ -26,7 +26,7 @@ import ./make-test-python.nix ({ pkgs, ...} : {
     };
 
   testScript = { nodes, ... }: let
-    user = nodes.machine.config.users.users.alice;
+    user = nodes.machine.users.users.alice;
   in ''
       machine.wait_for_x()
       machine.wait_for_file("${user.home}/.Xauthority")
diff --git a/nixos/tests/yggdrasil.nix b/nixos/tests/yggdrasil.nix
index b60a0e6b06c..eaf14e29acb 100644
--- a/nixos/tests/yggdrasil.nix
+++ b/nixos/tests/yggdrasil.nix
@@ -10,8 +10,13 @@ let
     InterfacePeers = {
       eth1 = [ "tcp://192.168.1.200:12345" ];
     };
-    MulticastInterfaces = [ "eth1" ];
-    LinkLocalTCPPort = 54321;
+    MulticastInterfaces = [ {
+      Regex = ".*";
+      Beacon = true;
+      Listen = true;
+      Port = 54321;
+      Priority = 0;
+    } ];
     PublicKey = "2b6f918b6c1a4b54d6bcde86cf74e074fb32ead4ee439b7930df2aa60c825186";
     PrivateKey = "0c4a24acd3402722ce9277ed179f4a04b895b49586493c25fbaed60653d857d62b6f918b6c1a4b54d6bcde86cf74e074fb32ead4ee439b7930df2aa60c825186";
   };
@@ -115,8 +120,12 @@ in import ./make-test-python.nix ({ pkgs, ...} : {
           settings = {
             IfTAPMode = true;
             IfName = "ygg0";
-            MulticastInterfaces = [ "eth1" ];
-            LinkLocalTCPPort = 43210;
+            MulticastInterfaces = [
+              {
+                Port = 43210;
+              }
+            ];
+            openMulticastPort = true;
           };
           persistentKeys = true;
         };
diff --git a/nixos/tests/zfs.nix b/nixos/tests/zfs.nix
index 29df691cecb..8e52e006574 100644
--- a/nixos/tests/zfs.nix
+++ b/nixos/tests/zfs.nix
@@ -12,108 +12,180 @@ let
                       then pkgs.zfsUnstable.latestCompatibleLinuxPackages
                       else pkgs.linuxPackages
     , enableUnstable ? false
+    , enableSystemdStage1 ? false
     , extraTest ? ""
     }:
     makeTest {
       name = "zfs-" + name;
       meta = with pkgs.lib.maintainers; {
-        maintainers = [ adisbladis ];
+        maintainers = [ adisbladis elvishjerricco ];
       };
 
       nodes.machine = { pkgs, lib, ... }:
         let
           usersharePath = "/var/lib/samba/usershares";
         in {
-        virtualisation.emptyDiskImages = [ 4096 ];
+        virtualisation = {
+          emptyDiskImages = [ 4096 4096 ];
+          useBootLoader = true;
+          useEFIBoot = true;
+        };
+        boot.loader.systemd-boot.enable = true;
+        boot.loader.timeout = 0;
+        boot.loader.efi.canTouchEfiVariables = true;
         networking.hostId = "deadbeef";
         boot.kernelPackages = kernelPackage;
         boot.supportedFilesystems = [ "zfs" ];
         boot.zfs.enableUnstable = enableUnstable;
+        boot.initrd.systemd.enable = enableSystemdStage1;
 
-        services.samba = {
-          enable = true;
-          extraConfig = ''
-            registry shares = yes
-            usershare path = ${usersharePath}
-            usershare allow guests = yes
-            usershare max shares = 100
-            usershare owner only = no
-          '';
+        environment.systemPackages = [ pkgs.parted ];
+
+        # /dev/disk/by-id doesn't get populated in the NixOS test framework
+        boot.zfs.devNodes = "/dev/disk/by-uuid";
+
+        specialisation.samba.configuration = {
+          services.samba = {
+            enable = true;
+            extraConfig = ''
+              registry shares = yes
+              usershare path = ${usersharePath}
+              usershare allow guests = yes
+              usershare max shares = 100
+              usershare owner only = no
+            '';
+          };
+          systemd.services.samba-smbd.serviceConfig.ExecStartPre =
+            "${pkgs.coreutils}/bin/mkdir -m +t -p ${usersharePath}";
+          virtualisation.fileSystems = {
+            "/tmp/mnt" = {
+              device = "rpool/root";
+              fsType = "zfs";
+            };
+          };
         };
-        systemd.services.samba-smbd.serviceConfig.ExecStartPre =
-          "${pkgs.coreutils}/bin/mkdir -m +t -p ${usersharePath}";
 
-        environment.systemPackages = [ pkgs.parted ];
+        specialisation.encryption.configuration = {
+          boot.zfs.requestEncryptionCredentials = [ "automatic" ];
+          virtualisation.fileSystems."/automatic" = {
+            device = "automatic";
+            fsType = "zfs";
+          };
+          virtualisation.fileSystems."/manual" = {
+            device = "manual";
+            fsType = "zfs";
+          };
+          virtualisation.fileSystems."/manual/encrypted" = {
+            device = "manual/encrypted";
+            fsType = "zfs";
+            options = [ "noauto" ];
+          };
+          virtualisation.fileSystems."/manual/httpkey" = {
+            device = "manual/httpkey";
+            fsType = "zfs";
+            options = [ "noauto" ];
+          };
+        };
 
-        # Setup regular fileSystems machinery to ensure forceImportAll can be
-        # tested via the regular service units.
-        virtualisation.fileSystems = {
-          "/forcepool" = {
+        specialisation.forcepool.configuration = {
+          systemd.services.zfs-import-forcepool.wantedBy = lib.mkVMOverride [ "forcepool.mount" ];
+          systemd.targets.zfs.wantedBy = lib.mkVMOverride [];
+          boot.zfs.forceImportAll = true;
+          virtualisation.fileSystems."/forcepool" = {
             device = "forcepool";
             fsType = "zfs";
             options = [ "noauto" ];
           };
         };
 
-        # forcepool doesn't exist at first boot, and we need to manually test
-        # the import after tweaking the hostId.
-        systemd.services.zfs-import-forcepool.wantedBy = lib.mkVMOverride [];
-        systemd.targets.zfs.wantedBy = lib.mkVMOverride [];
-        boot.zfs.forceImportAll = true;
-        # /dev/disk/by-id doesn't get populated in the NixOS test framework
-        boot.zfs.devNodes = "/dev/disk/by-uuid";
+        services.nginx = {
+          enable = true;
+          virtualHosts = {
+            localhost = {
+              locations = {
+                "/zfskey" = {
+                  return = ''200 "httpkeyabc"'';
+                };
+              };
+            };
+          };
+        };
       };
 
       testScript = ''
+        machine.wait_for_unit("multi-user.target")
         machine.succeed(
-            "modprobe zfs",
             "zpool status",
-            "ls /dev",
-            "mkdir /tmp/mnt",
-            "udevadm settle",
             "parted --script /dev/vdb mklabel msdos",
             "parted --script /dev/vdb -- mkpart primary 1024M -1s",
-            "udevadm settle",
-            "zpool create rpool /dev/vdb1",
-            "zfs create -o mountpoint=legacy rpool/root",
-            # shared datasets cannot have legacy mountpoint
-            "zfs create rpool/shared_smb",
-            "mount -t zfs rpool/root /tmp/mnt",
-            "udevadm settle",
-            # wait for samba services
-            "systemctl is-system-running --wait",
-            "zfs set sharesmb=on rpool/shared_smb",
-            "zfs share rpool/shared_smb",
-            "smbclient -gNL localhost | grep rpool_shared_smb",
-            "umount /tmp/mnt",
-            "zpool destroy rpool",
-            "udevadm settle",
+            "parted --script /dev/vdc mklabel msdos",
+            "parted --script /dev/vdc -- mkpart primary 1024M -1s",
         )
 
-        machine.succeed(
-            'echo password | zpool create -o altroot="/tmp/mnt" '
-            + "-O encryption=aes-256-gcm -O keyformat=passphrase rpool /dev/vdb1",
-            "zfs create -o mountpoint=legacy rpool/root",
-            "mount -t zfs rpool/root /tmp/mnt",
-            "udevadm settle",
-            "umount /tmp/mnt",
-            "zpool destroy rpool",
-            "udevadm settle",
-        )
+        with subtest("sharesmb works"):
+            machine.succeed(
+                "zpool create rpool /dev/vdb1",
+                "zfs create -o mountpoint=legacy rpool/root",
+                # shared datasets cannot have legacy mountpoint
+                "zfs create rpool/shared_smb",
+                "bootctl set-default nixos-generation-1-specialisation-samba.conf",
+                "sync",
+            )
+            machine.crash()
+            machine.wait_for_unit("multi-user.target")
+            machine.succeed(
+                "zfs set sharesmb=on rpool/shared_smb",
+                "zfs share rpool/shared_smb",
+                "smbclient -gNL localhost | grep rpool_shared_smb",
+                "umount /tmp/mnt",
+                "zpool destroy rpool",
+            )
+
+        with subtest("encryption works"):
+            machine.succeed(
+                'echo password | zpool create -O mountpoint=legacy '
+                + "-O encryption=aes-256-gcm -O keyformat=passphrase automatic /dev/vdb1",
+                "zpool create -O mountpoint=legacy manual /dev/vdc1",
+                "echo otherpass | zfs create "
+                + "-o encryption=aes-256-gcm -o keyformat=passphrase manual/encrypted",
+                "zfs create -o encryption=aes-256-gcm -o keyformat=passphrase "
+                + "-o keylocation=http://localhost/zfskey manual/httpkey",
+                "bootctl set-default nixos-generation-1-specialisation-encryption.conf",
+                "sync",
+                "zpool export automatic",
+                "zpool export manual",
+            )
+            machine.crash()
+            machine.start()
+            machine.wait_for_console_text("Starting password query on")
+            machine.send_console("password\n")
+            machine.wait_for_unit("multi-user.target")
+            machine.succeed(
+                "zfs get -Ho value keystatus manual/encrypted | grep -Fx unavailable",
+                "echo otherpass | zfs load-key manual/encrypted",
+                "systemctl start manual-encrypted.mount",
+                "zfs load-key manual/httpkey",
+                "systemctl start manual-httpkey.mount",
+                "umount /automatic /manual/encrypted /manual/httpkey /manual",
+                "zpool destroy automatic",
+                "zpool destroy manual",
+            )
 
         with subtest("boot.zfs.forceImportAll works"):
             machine.succeed(
                 "rm /etc/hostid",
                 "zgenhostid deadcafe",
                 "zpool create forcepool /dev/vdb1 -O mountpoint=legacy",
+                "bootctl set-default nixos-generation-1-specialisation-forcepool.conf",
+                "rm /etc/hostid",
+                "sync",
             )
-            machine.shutdown()
-            machine.start()
-            machine.succeed("udevadm settle")
+            machine.crash()
+            machine.wait_for_unit("multi-user.target")
             machine.fail("zpool import forcepool")
             machine.succeed(
-                "systemctl start zfs-import-forcepool.service",
-                "mount -t zfs forcepool /tmp/mnt",
+                "systemctl start forcepool.mount",
+                "mount | grep forcepool",
             )
       '' + extraTest;
 
@@ -128,6 +200,11 @@ in {
     enableUnstable = true;
   };
 
+  unstableWithSystemdStage1 = makeZfsTest "unstable" {
+    enableUnstable = true;
+    enableSystemdStage1 = true;
+  };
+
   installer = (import ./installer.nix { }).zfsroot;
 
   expand-partitions = makeTest {
diff --git a/nixos/tests/zram-generator.nix b/nixos/tests/zram-generator.nix
new file mode 100644
index 00000000000..2be7bd2e05b
--- /dev/null
+++ b/nixos/tests/zram-generator.nix
@@ -0,0 +1,42 @@
+import ./make-test-python.nix {
+  name = "zram-generator";
+
+  nodes = {
+    single = { ... }: {
+      virtualisation = {
+        emptyDiskImages = [ 512 ];
+      };
+      zramSwap = {
+        enable = true;
+        priority = 10;
+        algorithm = "lz4";
+        swapDevices = 1;
+        memoryPercent = 30;
+        memoryMax = 10 * 1024 * 1024;
+        writebackDevice = "/dev/vdb";
+      };
+    };
+    machine = { ... }: {
+      zramSwap = {
+        enable = true;
+        priority = 10;
+        algorithm = "lz4";
+        swapDevices = 2;
+        memoryPercent = 30;
+        memoryMax = 10 * 1024 * 1024;
+      };
+    };
+  };
+
+  testScript = ''
+    single.wait_for_unit("systemd-zram-setup@zram0.service")
+
+    machine.wait_for_unit("systemd-zram-setup@zram0.service")
+    machine.wait_for_unit("systemd-zram-setup@zram1.service")
+    zram = machine.succeed("zramctl --noheadings --raw")
+    swap = machine.succeed("swapon --show --noheadings")
+    for i in range(2):
+        assert f"/dev/zram{i} lz4 10M" in zram
+        assert f"/dev/zram{i} partition  10M" in swap
+  '';
+}